Skip to main content
The workflow runtime routes start runs of a deployed workflow, read them, and recover them when they park. Definitions are authored with lua push workflow and activated with lua workflows deploy; these routes only run them. This page covers the run model, the definition and run routes, recovery, and the error codes. Steps, events, artifacts, and exports are on Workflow events; approvals, signals, and gates on Workflow approvals; compose, schedules, goals, workspaces, and organization-wide routes on Workflow schedules and goals. Every route is the REST twin of a Workflows method or a lua workflows action, named under each endpoint. The CLI equivalents on this page require lua-cli 3.36.0 or later.

Base URL and authentication

Requests go to https://api.heylua.ai with Authorization: Bearer <<YOUR_API_KEY>>; the REST API overview covers keys, the typed error envelope, cursor pagination, and the per-minute rate buckets. Per-agent routes live under /workflows/:agentId and bind their scope to that agent: workflows:read for every GET, workflows:execute to start runs and act on them, workflows:write to compose, schedule, create or edit goals, and start a run from an inline graph or script. Reading step inputs and outputs needs workflows:read-outputs; without it payload fields come back as { "__hidden": true } and event data is redacted. Organization-wide routes bind to the orgId you pass. Approving, denying, editing an approval payload, resolving a parked step, erasing a run, and reassigning assets are human-only: a typed API credential is refused with 403 APPROVAL_REQUIRES_HUMAN, so drive those from a user session or the CLI. Where an endpoint names a Workflows method, the method works in deployed agents and under lua test alike, except Workflows.raiseBudget, which no runtime serves; the deployed members return less than the typings declare (see Deployed runtime differences).

Run model

A run is identified by runId (wfr_...), pins the workflowVersionId it started on, and finishes on that version: deploying a new version never moves a live run. It moves through these statuses: abandoned is a run a person or an agent force-canceled once forceAvailableAt had passed; timed_out is a run that reached budget.maxDurationSeconds, idle or in flight. A gated or suspended run carries gate { kind, since, expiresAt?, stepId?, attempt?, code? } with kind one of start-consent (approvalLinkId), quota (code concurrency_cap or suspended_cap), billing (code, operationId), budget (dimension, spent, cap), exception, org_archived, disabled, or model_policy (code, stepId: a step whose model request the organization’s policy cannot resolve, parked rather than failed); Gates says which verb clears each. trigger records who started the run: chat, sdk, api (CLI and HTTP), schedule, webhook (a trigger primitive), template, workflow (a parent run), or device. origin records what it ran: inline, compose, definition, schedule, or goal. A run summary, the row every list returns, carries runId, workflowId, workflowName, workflowVersionId, form (graph or script), agentId, orgId, status, gate, trigger, origin, createdBy { subjectType, subjectId }, counts { total, completed, failed, skipped, running, suspended, pending, cancelled? }, createdAt, startedAt, updatedAt, completedAt, durationMs (terminal only), correlationKey, tags, principalKind (user, service, or customer), batchId, goalId, cancel, usage, error { code, message, stepId?, issues? }, hasOutput, restricted, stepCount, currentLabel, phase, and eventSeq. Timestamps are Unix time in milliseconds. The detail adds input, output (inline up to 256 KB, else { "__cdnRef" } beside an outputPreview and an outputRef), steps[], edges[], suspensions[], budget { maxCredits?, maxSteps, maxDurationSeconds, deadlineAt, unit, spent { credits, actionsEstimate, steps }, reserved, remaining? }, budgetRaises[], and, on a run whose steps were assigned models, modelConsent { plan, card }: the per-step model plan and the rendered lines of the card a person consents to. An agent step row carries taskClass, modelRequested, modelResolved, modelReason, effortApplied, and modelFallback[], each absent when the step does not have it; see Workflow events. The detail of a run that needed consent to start also carries consent { approvedBy, at, approvalLinkId?, via?, envelope? }. via is human when a person approved it and autonomy when the organization’s pre-consent envelope admitted it; an autonomous start has no approvalLinkId, records system:policy as approvedBy, and carries the resolved envelope that let it through.

Definitions

GET /workflows/:agentId/definitions

Lists the workflow definitions on an agent.
string
required
The agent.
string
graph or script.
boolean
default:"false"
Include chat-composed workflows.
string
nextCursor from the previous page.
integer
default:"50"
From 1 to 100.
Response 200 with { "items": [...], "nextCursor": string | null }; each item is { workflowId, name, description?, form, dynamic, active, activeVersionId?, inputSchema?, updatedAt, scheduleJob?, idleExpiresAt? }. Equivalent: lua workflows list.

GET /workflows/:agentId/definitions/:workflowId/rollup

Returns a per-day rollup of one workflow’s runs, bucketed by the UTC day the run was created.
string
Start of the range, as Unix milliseconds or an ISO 8601 timestamp; defaults to 30 days before to.
string
End of the range, same formats; defaults to now.
Response 200 with { "workflowId", "bucket": "day", "from", "to", "rows": [...] }; each row is { date, runs, completed, failed, cancelled, suspended, p50DurationMs, p95DurationMs, creditsTotal, stepFailures }, where date is YYYY-MM-DD, failed counts failed, timed_out, and abandoned runs, suspended counts suspended, gated, and waiting runs, and the percentiles are null on a day with no terminal run. The answer is cached for 60 seconds. 400 ROLLUP_RANGE_TOO_WIDE (maxDays, days) past 90 days, or VALIDATION_FAILED for a timestamp that does not parse or a from after to; 404 WORKFLOW_NOT_FOUND.

POST /workflows/:agentId/definitions/:workflowId/recompose

Rewrites a chat-composed workflow into the model-class form: reads its active version, turns every agent step whose stored task class maps to the class its current pin heads into model: "class/<class>", and publishes the result as a new version on the same definition. No model call is made, so the rewrite is reproducible and the version diff reviewable; a pin that differs from the class map is a deliberate override and is reported under skipped. Needs workflows:write.
boolean
true or 1: answer the same diff and write nothing.
Response 200 with { "workflowId", "fromVersionId", "fromVersion", "dryRun", "rewritten": [...], "skipped": [...], "composed"? }: the version the rewrite was computed from, the steps it rewrote and skipped, and composed only when a version was published (never on a dry run, and not when nothing was rewritten). 404 WORKFLOW_NOT_FOUND; 409 RECOMPOSE_NOT_APPLICABLE with reason one of not-dynamic (a pushed workflow), no-active-version, or no-graph; 503 MODEL_POLICY_UNVERIFIABLE or CONTROL_UNAVAILABLE. Equivalent: lua workflows recompose <workflow>.

Run routes

POST /workflows/:agentId/runs

Starts one run.
string
required
The agent.
string
Replay key; wins over the body field. Only this spelling is read: the channel routes’ X-Idempotency-Key is ignored here, so a retry that sends it creates a second run.
string
The saved definition, by name (at most 120 characters). Send exactly one of workflowName, workflowId, composeId, graph, or script.
string
The saved definition, by id.
string
A draft from POST .../compose.
object
An inline dynamic graph, at most 256 KB; needs workflows:write.
string
An inline script, at most 256 KB; needs workflows:write.
any
The run input, at most 256 KB; validated against the definition’s inputSchema.
string
At most 128 characters. The same key with the same definition and input replays the existing run; anything else answers 409 IDEMPOTENCY_KEY_REUSED.
object
{ maxCredits?, maxSteps?, maxDurationSeconds?, maxJobSeconds? }, each an integer of at least 1, capped by the organization’s policy. Omit a key rather than sending 0.
integer
From 0 to 55. Long-polls: when the run settles within the window the answer is 200 with the run detail instead of 202.
string
Pin a version; defaults to the active one.
string
A caller-chosen, non-unique key (at most 128 characters of letters, digits, :, _, -, ., /) for signals/by-key and list filters.
string[]
At most 10 tags of at most 40 characters.
string
default:"app"
Where the terminal summary goes: emailApp, email, app, or off.
object
{ channel, threadId } with channel one of whatsapp, sms, email, webchat, slack: the terminal summary is delivered on that channel. On slack it is posted through the agent’s own Slack bot; threadId is any Slack conversationId that Channels.send takes, usually C…:threadTs, or a Lua.request.conversation.externalId when the agent has more than one Slack connection. The run’s occurrence id is the idempotency key, so a redelivery never posts twice. With no Slack connection, or the bot not in the channel, the reply fails permanently (channel_unsupported, or thread_not_found for a thread Slack doesn’t know). Attachments go out as links.
string
For chat-started runs, the caller’s own thread on this agent where the run card lands (at most 256 characters); any other thread answers 400 ORIGIN_THREAD_INVALID.
boolean
Take one background agent turn on originThreadId when the run ends; without an origin thread the start is refused with 400 CALLBACK_THREAD_REQUIRED.
object
{ kind: "customer", externalId }: the run belongs to a customer, who can never be an approver.
object
{ repo?, ref?, credentialsRef?, sizeGb? } overriding a declared Job-tier workspace; sizeGb from 1 to 50.
object
Seeds the run state; at most 64 KB.
Response 202 with { "runId", "status": "queued" | "gated", "watchHint": { "events", "poll" } }. An idempotent replay answers 200 with the existing run’s runId, status, workflowId, workflowName, startedAt (ISO 8601), watchHint, and idempotentReplay: true, deduplicated: true; nothing was created. When waitSeconds elapsed on a settled run the answer is 200 with the run detail. Errors Equivalent: lua workflows start <workflow> --input @input.json --follow, Workflows.start().

POST /workflows/:agentId/runs/batch

Starts many runs of one workflow at once. The body may be at most 2 MB.
string
required
The definition.
array
required
From 1 to 200 items, subject to the organization’s batch cap: { idempotencyKey (required, at most 128 characters, unique within the batch), input?, budget?, tags?, correlationKey? }.
string
default:"gate"
gate parks items over the concurrency cap; reject refuses them.
object
Batch-wide default; an item’s own budget wins.
string
Pin a version.
Response 202 with { "batchId", "accepted", "items": [{ idempotencyKey, runId?, status, code?, gate?, issues? }] }, items in the order sent; status is queued, gated, replayed, or rejected, and accepted counts the first two. Errors 400 BATCH_TOO_LARGE (max, count) or BATCH_ITEM_INVALID (index, issues; also a duplicate idempotencyKey); 404 WORKFLOW_NOT_FOUND; 409 RUNS_IN_FLIGHT under forbid, or ORG_PURGING, either of which refuses the whole batch; 413 PAYLOAD_TOO_LARGE; 503 CONTROL_UNAVAILABLE, retry with the same keys. Equivalent: Workflows.startBatch().

GET /workflows/:agentId/runs

Lists runs on an agent, newest first by default. Rows carry metadata and counts, never outputs.
string
Comma-separated statuses.
string
Only runs of this definition.
string
One trigger value.
integer
Unix milliseconds; runs updated at or after it.
string
Runs of one batch start.
string
Exact match.
string
Repeatable, at most 10.
integer
Terminal runs only; maxDurationMs likewise.
string
default:"-createdAt"
-createdAt, createdAt, -durationMs, or durationMs.
string
nextCursor from the previous page.
integer
default:"50"
From 1 to 100.
Response 200 with { "items": [...run summaries], "nextCursor": string | null }. 400 BAD_CURSOR, CORRELATION_KEY_INVALID, or VALIDATION_FAILED with the offending parameter in issues[].path. Equivalent: lua workflows runs --workflow <name> --status failed, Workflows.list().

GET /workflows/:agentId/runs/:runId

Returns one run.
string
default:"summary"
full adds inputs and outputs for callers who may read them, plus receipt and notify for the creator or an org:manage holder.
Response 200 with the run detail. 404 RUN_NOT_FOUND; any other value of fields is 400 VALIDATION_FAILED. Equivalent: lua workflows status <runId> --steps, Workflows.get().

DELETE /workflows/:agentId/runs/:runId

Erases a terminal run with its events, steps, signals, and artifacts. Creator or organization admin (org:manage), person only. Response 202 with { "accepted": true, "purgeId" }. 403 APPROVAL_REQUIRES_HUMAN or NOT_RUN_CREATOR; 404 RUN_NOT_FOUND; 409 RUN_NOT_TERMINAL with nextAction: "cancel"; 501 ERASE_UNAVAILABLE; 503 CONTROL_UNAVAILABLE. Equivalent: lua workflows delete-run <runId> --yes.

Recovery

POST /workflows/:agentId/runs/:runId/cancel

Asks a run to stop at the next step boundary, and escalates to a force cancel when asked again after forceAvailableAt. Creator or a workflows:execute holder.
string
default:"request"
request or force.
string
At most 2,000 characters, kept for audit.
Response 200 with the verdict { "cancelRequested", "state", "status", "transitioned", "requestedAt"?, "nextAction": "cancel_again" | "force" | "none", "forceAvailableAt"?, "forced"? }. A force before forceAvailableAt is a 200 with transitioned: false and nextAction: "cancel_again", and a terminal run is a 200 with state: "terminal" and nextAction: "none"; neither is an error. 403 NOT_RUN_CREATOR; 404 RUN_NOT_FOUND; 503 CONTROL_UNAVAILABLE. Equivalent: lua workflows cancel <runId> --reason "wrong input", Workflows.cancel().

POST /workflows/:agentId/runs/:runId/steps/:stepId/retry

Re-arms a step that failed past its retries, parked on an exception gate, or is on a billing hold.
string
At most 2,000 characters.
boolean
Restart the step’s automatic retry budget; the attempt number keeps counting up. Ignored on a gate or billing park.
Response 200 with { "retried": true, "attempt", "runStatus", "remainingParks" }, or the no-op { "retried": false, "reason": "already_retried" | "already_resolved", "recorded": { at?, by?, outcome? }, "runStatus" } when a decision already closed the park. 404 RUN_NOT_FOUND or STEP_NOT_FOUND; 409 STEP_NOT_PARKED (stepId, status), STEP_RETRY_CAP (a retry that would run past attempt 20: stepId, attempt, toAttempt, maxAttempts), or RUN_TERMINAL. Equivalent: lua workflows retry-step <runId> --step <stepId> --note "vendor back up".

POST /workflows/:agentId/runs/:runId/steps/:stepId/resolve

Resolves a parked step by skipping it, completing it with an output you supply, or failing it. Person only.
string
default:"complete"
skip, complete, or fail.
any
Required for complete, ignored otherwise; at most 256 KB serialized; validated against the step’s outputSchema.
string
At most 2,000 characters.
Response 200 with { "resolved": true, "outcome", "runStatus", "remainingParks" }, or the no-op { "resolved": false, "reason": "already_resolved" | "already_retried", "recorded", "runStatus" }. 400 VALIDATION_FAILED (output-required) or RESOLVE_OUTPUT_INVALID (stepId, issues); 403 APPROVAL_REQUIRES_HUMAN or NOT_RUN_CREATOR; 404 RUN_NOT_FOUND or STEP_NOT_FOUND; 409 STEP_NOT_PARKED or RUN_TERMINAL; 413 OUTPUT_TOO_LARGE. Equivalent: lua workflows resolve-step <runId> --step <stepId> --outcome complete --output '{"sent":0}'.

POST /workflows/:agentId/runs/:runId/budget

Raises the budget of a run parked on a budget gate. Creator or organization admin.
number
New credit cap, above 0.
integer
New step cap, at least 1.
integer
New Job-tier seconds cap, at least 1.
integer
New wall-clock cap, at least 1.
string
At most 2,000 characters.
Response 200 with { "raised": true, "budget", "runStatus", "resumed" }, or { "raised": false, "reason": "already_raised", "runStatus" }; resumed: false means the raise still does not cover the next step’s reserve and the run parked again. 400 VALIDATION_FAILED (no cap sent, or a cap at or below the current one) or CAP_EXCEEDED (above the organization’s ceiling); 403 NOT_RUN_CREATOR; 409 BUDGET_NOT_RAISABLE when the run is not parked on a budget. Equivalent: lua workflows raise-budget <runId> --credits 20. Workflows.raiseBudget() is typed but never works: deployed code gets not_implemented (501) and lua test gets WORKFLOWS_API_UNAVAILABLE.

Error codes

Every code the four workflow pages can answer, by status.

See also

  • Workflows — the same operations from agent code, and what the deployed runtime serves
  • lua workflows — every action, with exit codes 4 to 8 for run outcomes
  • Workflow builder — the definitions these routes run
  • Operate runs — the day-two loop: watch, retry, resolve, export
  • Workflow approvals — answering the human input a run waits for