Harvest publishes an OpenAPI 3.1 document for the whole management API, so any language with an OpenAPI generator gets a typed client with no hand-written HTTP.
One source, three places it appears:
| Where | What |
|---|---|
GET {api_path}/openapi.json | Served by harvest_api_router. Read-only, and behind no admin gate. |
autumn-harvest-plugin/openapi.json | Compact. Compiled into the crate and served verbatim, so the endpoint runs no transform and can never fail. |
docs/openapi.json | The same document, pretty-printed, for offline codegen and review diffs. |
All three come from docs/api-contract.json. The chain is
contract to canonical route list to document, and a test pins every link. See
Guarantees.
The crate carries its own copy because a published crate can package no file
from outside its own directory. cargo xtask dev regenerate-openapi writes
both copies from one transform, and a test fails when either drifts.
If your app already serves /openapi.json
It keeps it. The route is nest-relative, so HarvestPlugin::api("/api/harvest")
puts this document at /api/harvest/openapi.json, and autumn-web's own
generated document stays at the root. The two describe different surfaces:
autumn-web's covers the routes your app declares with its route macros, and
this one covers the management API. openapi_spec::the_route_does_not_take_the_application_root_path
pins that.
Mounting the management router at the application root is the one case that
collides. Axum then rejects the duplicate GET /openapi.json at startup.
Use the published TypeScript client
Each GitHub release after 0.6.0 attaches autumn-harvest-client, a client
built from docs/openapi.json. You do not need a running
Harvest to install it or to write code against it. Replace
<version> with your server's version:
npm install https://github.com/autumn-foundation/autumn-harvest/releases/download/v<version>/autumn-harvest-client-<version>.tgz
The package version is the crate version. See
clients/typescript for the API and for
the routes whose responses have full types.
Generate a typed client in under ten minutes
The worked example lives in
examples/typescript-client, which
also says how to get a plugin running. With one serving
http://localhost:3000/api/harvest:
cd examples/typescript-client
npm ci
npm run generate # openapi-typescript reads the served document
npm run typecheck
npm start # starts a workflow and polls its status
src/start-and-poll.ts calls POST /workflows/{workflow_name}/start and
GET /workflows/{id} through openapi-fetch.
It contains no URL string, no method string and no fetch call.
Other generators read the same document:
# Python
openapi-python-client generate --url http://localhost:3000/api/harvest/openapi.json
# Go, Java, and 50 other targets. Use 7.x or newer: earlier versions reject
# OpenAPI 3.1 outright.
openapi-generator generate -i docs/openapi.json -g go -o ./harvest-client
Node 20 or newer for the TypeScript path. The first cargo run -p quickstart
compiles the workspace, which is the slow part; the client steps take seconds.
What the document carries
- Paths relative to the mount point.
servers[0].urlis/api/harvest, the conventional prefix. Change it whenHarvestPlugin::apimounts the router elsewhere. operationIdper operation, derived from the method and path (post_workflows_by_workflow_name_start). It is stable across regenerations, so a generated method name does not churn.- Parameters with explicit
requiredflags, for path, query and header. A repeatable query key is an exploded array. - A request-body schema for every route that takes a body. A route that takes none declares none, so a generated client does not send an empty object.
x-harvest-read-onlyandx-harvest-route-classon every operation. Both mirrorautumn_harvest::audit::CLASSIFIED_ROUTES, so tooling can separate reads from control-plane writes, and can see which routes are safe without any credential. The route category is also the operation tag.x-harvest-idempotency, where the contract records an idempotency rule.x-harvest-streamon everytext/event-streamroute. A blocking client that buffers a whole response would hang on such a route.- Response headers where the contract records them:
X-Harvest-Execution-Idon the by-id family,Retry-Afteron the204a still-running/resultreturns. - Security schemes for the two credentials the management API accepts:
HarvestBearerToken(scoped API tokens) andHarvestSessionCookie(the embedding application session). The top-levelsecuritylist also carries an empty requirement, because enforcement is the embedder's choice. Thepublic_saferoutes areGET /health,GET /health/live,GET /health/readyandGET /openapi.json. They override it withsecurity: [], which says positively that Harvest itself asks for no credential there. An embedder that wraps the router withHarvestPlugin::api_with_authgates every route, the document included, so a generator then needs a credential to read it. Seesecurity-posture.md.
Limits worth knowing
- Most response properties are open. The contract records field names, and
sometimes a type, not full JSON Schemas. A generator therefore types most
response properties as
unknown/any. The core lifecycle routes are the exception: every field declares a type. The list isautumn_harvest_plugin::openapi::CORE_CLIENT_ROUTES. On those routes, a field that holds any JSON value, such as workflow input, carriesx-harvest-any. A client sees it asunknown. Per-workflow message schemas are a different contract; seeGET /workflows/registered/{name}/schema. - Streaming routes declare
text/event-streamwith a string body. The frame grammar is in the response description. - Conditional shapes (the partial-availability envelopes of issue #756)
are described in prose on the response, not as a
oneOf. - Error responses carry no schema. The contract records a description per error status, not a body shape.
- No check reads the type of a hand-parsed query key. Every parameter
declares a type, and the transform rejects one without it. The audit checks
each
Query<T>field against the contract. A key parsed fromRawQueryhas a type set by hand. No check compares that type with the parser. - Inline schemas, no
$ref. Nothing is shared between operations, soopenapi-generatornames response models positionally. GET /workflows/by-id/{workflow_name}/{workflow_id}/childrendeclaresworkflow_nametwice: once as the parent path parameter, once as a query filter on the child type. It is legal OpenAPI, but a generator that flattens parameters into one argument list may emit two arguments with one name.
Regenerate the artifacts
cargo xtask dev regenerate-openapi
Run it whenever docs/api-contract.json changes. CI fails otherwise.
Guarantees
Six checks hold the chain together:
contract_regression::management_routes_match_contractfails whenmanagement_api_routes()and the contract disagree. That function is the canonical route list every registration surface is checked against.openapi_spec::document_covers_every_management_route_exactlyfails when the document misses a mounted route, or invents one.openapi_spec::checked_in_artifacts_match_the_generated_documentfails when either checked-in copy is stale, andopenapi_spec::served_endpoint_returns_the_documentfails when the endpoint serves anything else.- The
lintjob validates both copies withopenapi-spec-validator, and it runs on documentation-only pull requests too. cargo xtask audit openapi-response-coverage, also inlint, reads the handlers: every status a handler returns must be declared, every request-body field that is mandatory on the wire must be marked required, and every query key a hand-rolled parser matches must be documented. Every field serde accepts must be documented. The contract must mark a body required when the handler cannot run without it. EachQuery<T>field must be documented with its type and required flag. The audit also reads a body parsed from rawBytes. A type the audit cannot resolve fails the check.openapi_response_conformancedrives each core lifecycle route against Postgres. It asserts the status of each response shape, and checks every body against the published schema. An undeclared key, a wrong type, an undeclarednull, an array with no element type or a missing required key fails it.openapi_spec::core_client_routes_type_every_response_fieldfails when a core route loses a type.
Why the document is derived, not annotated
autumn-web builds an OpenAPI document from routes declared with its route
macros, which attach ApiDoc metadata to each handler.
harvest_api_router is a plain axum::Router, so its routes carry no such
metadata. Annotating the handlers would also record less than the contract
already holds: per-parameter required flags, per-route read-only class,
idempotency rules, and error responses. The contract is therefore the input,
and autumn-harvest-plugin/src/openapi.rs is the transform.