HTTP API
Authenticate, discover, and drive workflow runs over the Runyard Hub's HTTP API.
Every Runyard surface โ the web app, the CLI, and the MCP server โ is an
ordinary client of one HTTP API served by the Hub. Anything they can do, you
can do with curl.
Authentication
Send an access token as a bearer token on every request:
curl -H "Authorization: Bearer $RUNYARD_TOKEN" https://hub.example.com/api/meTokens are minted by an admin (web UI, POST /api/tokens, or the CLI) and carry
scopes:
| Scope | Grants |
|---|---|
api | Start runs, preflight, drafts, rerun/cancel/promote, create approvals |
mcp | The same operations, issued for MCP clients |
read | Read-only โ inspect workflows, runs, logs, artifacts, approvals, schedules, and runners; satisfies no mutation endpoint |
runner | The runner protocol: register, heartbeat, claim runs, report lifecycle events and artifacts |
approvals | Resolve approval cards (approve / reject / request changes) and create them |
admin | Superscope โ satisfies every scope requirement, plus admin-only surfaces: tokens, secrets, audit log, schedules, workflow create/update, packages, hooks, updates |
GET /api/tokens/scopes (admin) returns this vocabulary plus named presets โ
everything (api+mcp+approvals, the default), read-only,
approvals-only, runner, and admin โ which the Tokens page uses.
Reads such as listing runs, workflows, or artifacts require any authenticated
token. A token with only the read scope is therefore read-only: it can
inspect everything non-admin but every create/update/delete/run/approve call
fails with 403. A request with no valid token gets
401 with the standard error envelope; a token missing a required scope gets:
{
"error": {
"code": "insufficient_scope",
"message": "insufficient scope",
"status": 403,
"details": { "required": ["api", "mcp"] }
}
}with status 403. GET /api/me describes the calling token (name and scopes).
Discovery
GET /openapi.json(unauthenticated) โ generated from the server's API surface registry, the single source of truth every route is registered from, so every endpoint is documented there. This guide covers the important ones; use/openapi.jsonfor the full list.GET /api/menu(authenticated) โ the agent-facing menu: workflow catalog, local/remote execution modes, and follow-up paths. The same data backs the MCPget_menutool and/llms.txt.GET /api/version(unauthenticated) โ product name, version, instance name.
Versioning and deprecation policy
Operations are organized into OpenAPI tags: workflows, runs, work (Factory Items and boards), approvals, automation (schedules + workflow endpoints), ci, library (agents, skills, knowledge, hooks), distribution (bundles, packages), admin (tokens, secrets, audit, alerts, updates), and system (health, version, menu, dashboard, runners).
/api/v1 is the stable contract for HTTP agents. New clients should use
/api/v1/... paths. The existing canonical /api/... paths are compatibility
aliases for current clients; they remain supported, are not deprecated, and
have no sunset date.
Every grouped operation has a stable /api/v1 path that makes the group
visible โ for example:
| Compatibility path | Stable path |
|---|---|
GET /api/workflows | GET /api/v1/workflows |
GET /api/runs/:id | GET /api/v1/runs/:id |
GET /api/run-drafts | GET /api/v1/runs/drafts |
GET /api/schedules | GET /api/v1/automation/schedules |
GET /api/agents | GET /api/v1/library/agents |
GET /api/workflow-bundles | GET /api/v1/distribution/bundles |
GET /api/tokens | GET /api/v1/admin/tokens |
GET /api/menu | GET /api/v1/system/menu |
Both forms are the same registry entries registered twice, so they always
share the handler, auth, scopes, idempotency requirements, request schema, and
response schema. In openapi.json, /v1/... entries carry x-canonical-path
back to the compatibility path, while compatibility entries carry
x-stable-path to the stable /v1/... path.
Work-item status changes are factory automation boundaries. PATCH /api/factory-items/{id} and its /api/v1/factory/items/{id} alias run server-owned lane-enter triggers after a successful mutation: moving a proposal to a configured trigger.mode: "auto" lane launches the resolved workflow from the Hub, links the run to the ticket, and returns laneTriggers metadata. Preview the move first with GET /api/factory-items/{id}/status-preview?status=ready: auto lanes return the destination lane, trigger mode, resolved workflow, synthesized run input, duplicate/live-run suppression, and deterministic preflight. Callers that want negotiation can PATCH with { "status": "ready", "negotiate": true }; non-ready preflight creates a run draft and does not dispatch a run. The same behavior is used by Web, CLI, API, and MCP clients.
Every /api response includes machine-readable lifecycle headers:
| Header | Meaning |
|---|---|
X-Runyard-API-Version | Current contract version, currently v1 |
X-Runyard-API-Lifecycle | stable, compatibility, deprecated, or unversioned |
X-Runyard-API-Stable-Path | Present on compatibility aliases when a stable /api/v1 path exists |
Deprecation | Present only on deprecated routes |
Sunset | Present only after a route has a scheduled removal date |
The migration contract is concrete:
- A breaking HTTP change requires a new major prefix such as
/api/vN; the current/api/v1contract is never broken in place. - The release that introduces a deprecation must publish a written migration
guide, mark the affected operations in OpenAPI with
deprecated: trueandx-api-lifecycle, and returnDeprecation: trueon those routes. - Deprecated routes stay callable for at least 180 days after the first public deprecation notice.
- A route can only be removed after a
Sunsetheader has been returned for at least 90 days with the exact removal date. - Supported
/apicompatibility aliases are not deprecated merely because/api/v1exists. They do not sendDeprecationorSunsetheaders unless a future migration guide explicitly schedules them.
The legacy /api/capabilities/* paths are deprecated aliases of
/api/workflows/* and get no /api/v1 form; new clients should say
workflow everywhere.
The typical flow
List workflows, preflight your input, start the run, then poll it:
HUB=https://hub.example.com
AUTH="Authorization: Bearer $RUNYARD_TOKEN"
# 1. What can I run?
curl -H "$AUTH" "$HUB/api/workflows"
# 2. Dry-run the deterministic preflight โ nothing is created.
curl -H "$AUTH" -H "Content-Type: application/json" \
-d '{"input": {"topic": "hello", "title": "Hello run"}}' \
"$HUB/api/workflows/idea-to-product/preflight"
# 3. Start the run (or pass "negotiate": true to preflight-then-run in one call).
curl -H "$AUTH" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"input": {"topic": "hello", "title": "Hello run"}, "executionMode": "remote"}' \
"$HUB/api/workflows/idea-to-product/run"
# 4. Poll status, then read the timeline, logs, and artifacts.
curl -H "$AUTH" "$HUB/api/runs/<runId>"
curl -H "$AUTH" "$HUB/api/runs/<runId>/timeline"
curl -H "$AUTH" "$HUB/api/runs/<runId>/logs"
curl -H "$AUTH" "$HUB/api/runs/<runId>/artifacts"For agent-created runs, include a short human-readable input.title โ it labels
run lists, approval cards, and handoffs. executionMode is local or remote;
run also accepts an optional responseEndpoint ({type: http|telegram, config}) to have the terminal-state reply delivered when the run finishes
(polling /api/runs/:id remains the canonical fallback), and an optional
budget ({maxTokens?, maxCostMicros?}, also accepted as input.budget) that
hard-caps the run's metered model usage โ a breach terminates the run with the
distinct status budget_exceeded. Metered usage aggregates onto every run
object as usage and is detailed at GET /api/runs/:id/usage; see
Usage metering & budgets.
With "negotiate": true, a non-ready request returns the negotiation state
(questions, blockers, warnings, plus a saved draft) instead of creating a run โ
fix the input and call again.
Error responses
Newly-normalized API failures use a stable envelope:
{
"error": {
"code": "run_not_found",
"message": "run not found",
"status": 404,
"details": { "runId": "run_123" },
"requestId": "req_..."
}
}details and requestId are present only when the server has useful structured
context. The fixed code registry currently covers auth and scope failures,
JSON/body parsing failures, rate limits, run negotiation failures, missing
workflows, run-not-found responses, invalid SSE cursors, idempotency-key
failures, and budget validation/exceeded paths. Some older endpoints still
return legacy string errors until they are migrated.
Rate-limit metadata
Every /api response includes standard rate-limit headers for the bucket that
handled the request:
| Header | Semantics |
|---|---|
RateLimit-Limit | Maximum requests accepted by the active fixed-window bucket. |
RateLimit-Remaining | Requests remaining in that bucket after this response; 0 on 429. |
RateLimit-Reset | Whole seconds until the bucket resets. |
429 responses also keep Retry-After, using the same whole-second retry
delay as RateLimit-Reset. Header values are intentionally non-sensitive:
they never include bucket names, bucket keys, token identifiers, or client IPs.
Runner protocol requests use a separate high-capacity bucket from the general
API, so event streams, heartbeats, claims, and terminal reports cannot consume
the interactive API budget. Mandatory Idempotency-Key behavior is unchanged.
The generated AgentHubClient preserves existing return values by default. Pass
{ includeResponse: true } to receive { data, response, rateLimit }; thrown
HTTP errors expose error.rateLimit and error.retryAfter.
Idempotency keys
High-risk, non-idempotent agent-facing mutations require Idempotency-Key on
both their canonical paths and /api/v1 aliases. Generate one unique key for
each user intent, persist it with the request body, and reuse that same key for
transport retries. Same key + same body replays the stored response; same key +
different body returns 409.
Missing keys return a stable machine-readable error:
{
"error": {
"code": "missing_idempotency_key",
"message": "Idempotency-Key header is required for this request",
"status": 400
},
"code": "missing_idempotency_key"
}The required-key surface is deliberately narrow: workflow run dispatch,
submitting a run draft, manual schedule fire, work-item run link/unlink, and
approval decisions. Reads, preflights, and absolute-state updates such as
patching a workflow, schedule, board, Factory Item, or secret do not require a
key. Migration impact: HTTP agents that call those high-risk routes must add
the header before upgrading; MCP schemas mark idempotencyKey as required, and
the first-party CLI generates a UUID when the flag is omitted and prints it in
JSON output.
Key endpoints
Workflows and runs (preflight and run require api or mcp scope; reads
require any token):
| Method + path | Purpose |
|---|---|
GET /api/workflows | List workflows (?q= searches) |
GET /api/workflows/:id | Describe a workflow and its input schema |
GET /api/workflows/:id/source | Workflow source, parsed metadata, sections, graph |
POST /api/workflows/:id/preflight | Dry-run preflight: ready / needs_input / blocked, nothing created |
POST /api/workflows/:id/run | Run a workflow: {input, executionMode, negotiate?}; requires Idempotency-Key |
POST /api/workflows / PATCH /api/workflows/:id | Create/update a workflow (admin). Source is submitted as bytes or a workflow.bundleId โ never a file path |
GET /api/runs | List runs (filter by status, q, workflow) |
GET /api/runs/:id | Run status, outputs, error, response-delivery state |
GET /api/runs/:id/timeline | Unified ascending timeline (since=<iso>, limit=<n>); backs runyard tail |
GET /api/runs/:id/usage | Metered usage: aggregate totals, per-call records, budget, and budgetStop |
GET /api/runs/:id/events ยท /logs ยท /diagnostics | Structured events, log lines, diagnostics + log summary |
GET /api/runs/:id/artifacts ยท GET /api/artifacts/:id/download | List a run's artifacts; stream attachment bytes |
POST /api/runs/:id/cancel ยท /rerun ยท /promote | Cancel; re-queue with same/edited input; merge a successful isolated run into its target branch |
POST /api/runs/:id/pause ยท /resume | Park an active run on a recoverable interruption (e.g. credits_exhausted; records pause metadata + the engine checkpoint, frees the runner slot); resume re-queues the same run and continues from the checkpoint when one exists |
Artifact downloads stream bytes from disk and always use attachment
disposition so HTML/SVG artifacts are not rendered in the Hub origin. MCP
artifact reads are capped separately: inline text is limited by
RUNYARD_MCP_ARTIFACT_INLINE_BYTE_CAP (default 65536 bytes), while binary and
oversized artifacts return metadata and the authenticated download URL.
Other resources (see /openapi.json for the rest โ schedules, catalog
agents/skills/knowledge, hooks, workflow bundles and packages, workflow
endpoints, runners, dashboard):
| Method + path | Purpose |
|---|---|
GET /api/schedules ยท POST /api/schedules | List schedules; create cron/one-shot schedules (admin) |
GET /api/runners | Registered runners, heartbeat state, capacity, pool summary |
GET /api/tokens ยท POST /api/tokens ยท DELETE /api/tokens/:id | Manage access tokens (admin) |
GET /api/secrets ยท PUT /api/secrets/:key ยท DELETE /api/secrets/:key | Encrypted secrets store โ names and metadata only, never values (admin) |
GET /api/hooks | Post-run hook profiles a workflow may select via input.postRunHooks |
GET /api/audit | Audit log (admin) |
GET /api/dashboard | Run counts by status, recent activity, attention items |
Approvals
Workflows can pause on a human decision. Approval cards are first-class API
objects (resolving them requires api, mcp, or approvals scope):
| Method + path | Purpose |
|---|---|
GET /api/approvals?status=pending | List approval cards (pending or resolved) |
GET /api/approvals/:id | One card: title, ask (action / reason / audience), kind, linked run |
POST /api/approvals | Create a card: {title, description, runId?, ask, timeoutMs?/timeoutAt? + fallback?} for timed approvals |
POST /api/approvals/:id/approve | Approve โ a held run is released; requires Idempotency-Key |
POST /api/approvals/:id/reject | Reject โ a held run is cancelled (never failed); requires Idempotency-Key |
POST /api/approvals/:id/request-changes | Request changes with a comment; the run is cancelled so it can be re-run with new input; requires Idempotency-Key |
Run drafts
Drafts make run-creation negotiation explicit: a draft is a proposed run that has not been enqueued, and its status mirrors the latest preflight.
| Method + path | Purpose |
|---|---|
POST /api/run-drafts | Create + preflight a draft: {workflow, input, executionMode} โ 201 with status ready / needs_input / blocked and open questions |
GET /api/run-drafts ยท GET /api/run-drafts/:id | List / inspect drafts with their latest preflight report |
PATCH /api/run-drafts/:id | Answer questions: input is shallow-merged (a null value deletes a key; replaceInput: true replaces it), then re-preflighted |
POST /api/run-drafts/:id/submit | Re-preflight and enqueue the real run only when ready โ 202 with {run, draft}; requires Idempotency-Key |
POST /api/run-drafts/:id/discard | Abandon the negotiation |
Error conventions
| Status | Meaning |
|---|---|
401 | Missing/invalid token: {"error": "unauthorized"} |
403 | Insufficient scope: {"error": "insufficient scope", "required": [...]} (also runner-ownership failures on the runner protocol) |
422 | Negotiation needs_input โ the body carries the negotiation state: normalized input, questions, warnings, suggestedDefaults, nextAction |
409 | Negotiation blocked โ the body carries blockers; no run is created |
The 422/409 bodies are returned by run with negotiate: true and by
draft submit; they are structured responses, not opaque failures โ answer the
questions and retry.
Rate limits
A few endpoints are rate-limited per client IP (over the limit returns 429):
| Endpoint | Limit |
|---|---|
POST /api/auth/token-login | 10 / minute |
POST /api/auth/telegram-webapp | 30 / minute |
POST /api/schedules/:id/run-now | 60 / minute |
POST /api/chat (in-app assistant) | 60 / minute |