Workflows API
Run stored workflows from your own app. Every call except webhooks and resume URLs needs a gateway key with the workflows.run scope (an empty scope list allows everything). A key sees workflows in its own workspace plus global ones, and only those that are enabled, valid and have API callable switched on — a call to one without it is 403 workflow_not_callable.
#List workflows
GET /v1/workflows — the workflows this key may run: id, name, description, nodes (count) and the trigger's input_schema.
#Start a run
POST /v1/workflows/{idOrName}/runs — the path takes the workflow's id or its name.
{ "input": { "question": "Why was I charged twice?" }, "priority": 0 }
Returns 202 with the run (id, object: "workflow.run", workflow_id, workflow_name, version, status, step counters, usage, duration_ms, timestamps) and an X-Relay-Workflow-Run-Id header. Add ?wait=true to block until the run finishes (up to Gateway:Workflows:MaxSyncWaitSeconds, default 60): 200 with output when it completes, 502 when it fails, 202 if it's still going. Send an Idempotency-Key header to make retries safe — a repeat returns the original run.
#Read, cancel, watch
GET /v1/workflows/runs/{id}— status, counters, usage,output,error.POST /v1/workflows/runs/{id}/cancel— cooperative: the engine stops at the next step boundary.GET /v1/workflows/runs/{id}/events— server-sent events:stepwhen a node starts or settles,runon each status change,doneat the end with the final run. Lifecycle events, not tokens. At mostGateway:Workflows:MaxEventStreams(default 100) streams are open at once (429 too_many_streamsbeyond that), and a stream closes itself after an hour.
#Approvals
POST /v1/workflows/runs/{id}/approve and /reject, body { "comment": "…", "decided_by": "ana@contoso.com" }. decided_by falls back to X-Relay-User-Id, then to the key's name. Only a run waiting on an Approval node accepts a decision, and only the first decision counts (409 afterwards). The response is { id, approved, status: "queued" } — the run resumes on the engine's next pass.
#As a chat model
POST /v1/workflows/{idOrName}/chat/completions accepts an OpenAI chat request. trigger.input is { message, messages } — the last user message and the whole conversation. The reply is the output's text, answer, reply, content or message field, the output itself if it's a string, or its JSON; the response's model is workflow:<name>. stream: true returns a content chunk, a finish_reason: "stop" chunk, then [DONE]. A run still going after Gateway:Workflows:MaxSyncWaitSeconds is 504 workflow_timeout; a failed run is 502.
from openai import OpenAI
client = OpenAI(base_url="https://relay.example.com/v1/workflows/support-triage", api_key="app_live_…")
print(client.chat.completions.create(model="workflow", messages=[{"role": "user", "content": "hi"}]).choices[0].message.content)
#Webhooks (no key)
/v1/workflows/hooks/{token} — the webhook trigger's URL. The trigger decides which method it accepts (POST by default, or GET / PUT / PATCH / DELETE / any); another method is 405 with an Allow header. The token is the credential: an unknown token and a disabled webhook both return the same 404, so tokens cannot be probed.
trigger.input is { method, query, headers, body }, with Authorization, Cookie, X-Api-Key and hop-by-hop headers removed. Bodies are capped (Gateway:Workflows:Webhook:MaxBodyBytes, 1 MB → 413 body_too_large), calls are rate-limited per token (:RatePerMinute, 60 → 429 rate_limited), and an Idempotency-Key header is honoured. With a Signing secret credential on the trigger, the HMAC-SHA256 of the body is checked against the configured header (default X-Hub-Signature-256, sha256= prefix allowed) and a mismatch is 401 signature_invalid. Accepted calls and signature rejections are audited.
The response depends on the trigger's Respond setting: 202 with the run id immediately; the run's output when it finishes; or whatever a Respond to webhook node sets. The two waiting modes give up after Gateway:Workflows:MaxSyncWaitSeconds and return 202 { id, status: "running" }; a run that fails while the caller waits is 500, and a Respond-node run that ends without reaching the node is 500 no_response.
#Resume URLs (no key)
GET|POST /v1/workflows/resume/{token} — the {{ run.resumeUrl }} of a run waiting on a Wait node in webhook mode. The request becomes the Wait node's payload as { method, query, headers, body }, with the same body cap and rate limit as hooks. Returns 202 { resumed: true, id }. Single use: once the run resumes, the URL returns 404 resume_not_found.