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.
Verified against lua-cli 3.33.0.
Base URL and authentication
Requests go tohttps://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, deployed agents serve only start, get, list, and cancel; the other members work under lua test and throw not_implemented when deployed (see Deployed runtime differences).
Run model
A run is identified byrunId (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, or disabled; 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? }, and budgetRaises[].
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.
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.
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.
Run routes
POST /workflows/:agentId/runs
Starts one run.string
required
The agent.
string
Replay key; wins over the body field.
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.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.
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.
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.
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.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 afterforceAvailableAt. Creator or a workflows:execute holder.
string
default:"request"
request or force.string
At most 2,000 characters, kept for audit.
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.
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.
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 abudget 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.
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 serveslua 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

