Runyard Docs
Guides

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/me

Tokens are minted by an admin (web UI, POST /api/tokens, or the CLI) and carry scopes:

ScopeGrants
apiStart runs, preflight, drafts, rerun/cancel/promote, create approvals
mcpThe same operations, issued for MCP clients
readRead-only โ€” inspect workflows, runs, logs, artifacts, approvals, schedules, and runners; satisfies no mutation endpoint
runnerThe runner protocol: register, heartbeat, claim runs, report lifecycle events and artifacts
approvalsResolve approval cards (approve / reject / request changes) and create them
adminSuperscope โ€” 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.json for 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 MCP get_menu tool 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 pathStable path
GET /api/workflowsGET /api/v1/workflows
GET /api/runs/:idGET /api/v1/runs/:id
GET /api/run-draftsGET /api/v1/runs/drafts
GET /api/schedulesGET /api/v1/automation/schedules
GET /api/agentsGET /api/v1/library/agents
GET /api/workflow-bundlesGET /api/v1/distribution/bundles
GET /api/tokensGET /api/v1/admin/tokens
GET /api/menuGET /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:

HeaderMeaning
X-Runyard-API-VersionCurrent contract version, currently v1
X-Runyard-API-Lifecyclestable, compatibility, deprecated, or unversioned
X-Runyard-API-Stable-PathPresent on compatibility aliases when a stable /api/v1 path exists
DeprecationPresent only on deprecated routes
SunsetPresent only after a route has a scheduled removal date

The migration contract is concrete:

  1. A breaking HTTP change requires a new major prefix such as /api/vN; the current /api/v1 contract is never broken in place.
  2. The release that introduces a deprecation must publish a written migration guide, mark the affected operations in OpenAPI with deprecated: true and x-api-lifecycle, and return Deprecation: true on those routes.
  3. Deprecated routes stay callable for at least 180 days after the first public deprecation notice.
  4. A route can only be removed after a Sunset header has been returned for at least 90 days with the exact removal date.
  5. Supported /api compatibility aliases are not deprecated merely because /api/v1 exists. They do not send Deprecation or Sunset headers 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:

HeaderSemantics
RateLimit-LimitMaximum requests accepted by the active fixed-window bucket.
RateLimit-RemainingRequests remaining in that bucket after this response; 0 on 429.
RateLimit-ResetWhole 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 + pathPurpose
GET /api/workflowsList workflows (?q= searches)
GET /api/workflows/:idDescribe a workflow and its input schema
GET /api/workflows/:id/sourceWorkflow source, parsed metadata, sections, graph
POST /api/workflows/:id/preflightDry-run preflight: ready / needs_input / blocked, nothing created
POST /api/workflows/:id/runRun a workflow: {input, executionMode, negotiate?}; requires Idempotency-Key
POST /api/workflows / PATCH /api/workflows/:idCreate/update a workflow (admin). Source is submitted as bytes or a workflow.bundleId โ€” never a file path
GET /api/runsList runs (filter by status, q, workflow)
GET /api/runs/:idRun status, outputs, error, response-delivery state
GET /api/runs/:id/timelineUnified ascending timeline (since=<iso>, limit=<n>); backs runyard tail
GET /api/runs/:id/usageMetered usage: aggregate totals, per-call records, budget, and budgetStop
GET /api/runs/:id/events ยท /logs ยท /diagnosticsStructured events, log lines, diagnostics + log summary
GET /api/runs/:id/artifacts ยท GET /api/artifacts/:id/downloadList a run's artifacts; stream attachment bytes
POST /api/runs/:id/cancel ยท /rerun ยท /promoteCancel; re-queue with same/edited input; merge a successful isolated run into its target branch
POST /api/runs/:id/pause ยท /resumePark 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 + pathPurpose
GET /api/schedules ยท POST /api/schedulesList schedules; create cron/one-shot schedules (admin)
GET /api/runnersRegistered runners, heartbeat state, capacity, pool summary
GET /api/tokens ยท POST /api/tokens ยท DELETE /api/tokens/:idManage access tokens (admin)
GET /api/secrets ยท PUT /api/secrets/:key ยท DELETE /api/secrets/:keyEncrypted secrets store โ€” names and metadata only, never values (admin)
GET /api/hooksPost-run hook profiles a workflow may select via input.postRunHooks
GET /api/auditAudit log (admin)
GET /api/dashboardRun 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 + pathPurpose
GET /api/approvals?status=pendingList approval cards (pending or resolved)
GET /api/approvals/:idOne card: title, ask (action / reason / audience), kind, linked run
POST /api/approvalsCreate a card: {title, description, runId?, ask, timeoutMs?/timeoutAt? + fallback?} for timed approvals
POST /api/approvals/:id/approveApprove โ€” a held run is released; requires Idempotency-Key
POST /api/approvals/:id/rejectReject โ€” a held run is cancelled (never failed); requires Idempotency-Key
POST /api/approvals/:id/request-changesRequest 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 + pathPurpose
POST /api/run-draftsCreate + preflight a draft: {workflow, input, executionMode} โ†’ 201 with status ready / needs_input / blocked and open questions
GET /api/run-drafts ยท GET /api/run-drafts/:idList / inspect drafts with their latest preflight report
PATCH /api/run-drafts/:idAnswer questions: input is shallow-merged (a null value deletes a key; replaceInput: true replaces it), then re-preflighted
POST /api/run-drafts/:id/submitRe-preflight and enqueue the real run only when ready โ€” 202 with {run, draft}; requires Idempotency-Key
POST /api/run-drafts/:id/discardAbandon the negotiation

Error conventions

StatusMeaning
401Missing/invalid token: {"error": "unauthorized"}
403Insufficient scope: {"error": "insufficient scope", "required": [...]} (also runner-ownership failures on the runner protocol)
422Negotiation needs_input โ€” the body carries the negotiation state: normalized input, questions, warnings, suggestedDefaults, nextAction
409Negotiation 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):

EndpointLimit
POST /api/auth/token-login10 / minute
POST /api/auth/telegram-webapp30 / minute
POST /api/schedules/:id/run-now60 / minute
POST /api/chat (in-app assistant)60 / minute

See also

  • MCP guide โ€” every API operation is also an MCP tool
  • CLI guide โ€” the same API from your terminal

On this page