> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heylua.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Workflows

> Run model, definition and run routes, recovery verbs, and every error code of the workflow runtime API

The workflow runtime routes start [runs](/concepts/workflows) of a deployed workflow, read them, and recover them when they park. Definitions are authored with [`lua push workflow`](/reference/cli/push) and activated with [`lua workflows deploy`](/reference/cli/workflows); 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](/reference/rest/workflow-events); approvals, signals, and gates on [Workflow approvals](/reference/rest/workflow-approvals); compose, schedules, goals, workspaces, and organization-wide routes on [Workflow schedules and goals](/reference/rest/workflow-schedules-and-goals). Every route is the REST twin of a [`Workflows`](/reference/sdk/workflows) method or a `lua workflows` action, named under each endpoint.

*Verified against lua-cli 3.33.0.*

## Base URL and authentication

Requests go to `https://api.heylua.ai` with `Authorization: Bearer <<YOUR_API_KEY>>`; the [REST API overview](/reference/rest/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](/reference/sdk/workflows#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:

| Group     | Statuses                                                     | Meaning                                                        |
| --------- | ------------------------------------------------------------ | -------------------------------------------------------------- |
| In flight | `queued`, `running`, `cancellation_requested`                | The run holds an execution slot                                |
| Idle      | `gated`, `suspended`, `waiting`                              | Nothing executes until a person, a signal, or a timer wakes it |
| Terminal  | `completed`, `failed`, `cancelled`, `abandoned`, `timed_out` | Final; late writes are fenced off                              |

`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](/reference/rest/workflow-approvals#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.

<ParamField path="agentId" type="string" required>The agent.</ParamField>
<ParamField query="form" type="string">`graph` or `script`.</ParamField>
<ParamField query="includeDynamic" type="boolean" default="false">Include chat-composed workflows.</ParamField>
<ParamField query="cursor" type="string">`nextCursor` from the previous page.</ParamField>
<ParamField query="limit" type="integer" default="50">From 1 to 100.</ParamField>

**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`.

<CodeGroup>
  ```bash CLI theme={null}
  lua workflows list
  ```

  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/workflows/<<YOUR_AGENT_ID>>/definitions?limit=50', {
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>' },
  });
  const page: { items: Array<{ workflowId: string; name: string; active: boolean }>; nextCursor: string | null } =
    await response.json();
  for (const definition of page.items) console.log(definition.name, definition.active);
  ```

  ```bash cURL theme={null}
  curl "https://api.heylua.ai/workflows/<<YOUR_AGENT_ID>>/definitions?limit=50" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>"
  ```
</CodeGroup>

### 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.

<ParamField query="from" type="string">Start of the range, as Unix milliseconds or an ISO 8601 timestamp; defaults to 30 days before `to`.</ParamField>
<ParamField query="to" type="string">End of the range, same formats; defaults to now.</ParamField>

**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`.

## Run routes

### POST /workflows/:agentId/runs

Starts one run.

<ParamField path="agentId" type="string" required>The agent.</ParamField>
<ParamField header="Idempotency-Key" type="string">Replay key; wins over the body field.</ParamField>
<ParamField body="workflowName" type="string">The saved definition, by name (at most 120 characters). Send exactly one of `workflowName`, `workflowId`, `composeId`, `graph`, or `script`.</ParamField>
<ParamField body="workflowId" type="string">The saved definition, by id.</ParamField>
<ParamField body="composeId" type="string">A draft from `POST .../compose`.</ParamField>
<ParamField body="graph" type="object">An inline dynamic graph, at most 256 KB; needs `workflows:write`.</ParamField>
<ParamField body="script" type="string">An inline script, at most 256 KB; needs `workflows:write`.</ParamField>
<ParamField body="input" type="any">The run input, at most 256 KB; validated against the definition's `inputSchema`.</ParamField>

<ParamField body="idempotencyKey" type="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`.
</ParamField>

<ParamField body="budget" type="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.
</ParamField>

<ParamField body="waitSeconds" type="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`.
</ParamField>

<ParamField body="workflowVersionId" type="string">Pin a version; defaults to the active one.</ParamField>

<ParamField body="correlationKey" type="string">
  A caller-chosen, non-unique key (at most 128 characters of letters, digits, `:`, `_`, `-`, `.`, `/`) for `signals/by-key` and list filters.
</ParamField>

<ParamField body="tags" type="string[]">At most 10 tags of at most 40 characters.</ParamField>
<ParamField body="notify" type="string" default="app">Where the terminal summary goes: `emailApp`, `email`, `app`, or `off`.</ParamField>
<ParamField body="replyTo" type="object">`{ channel, threadId }` with `channel` one of `whatsapp`, `sms`, `email`, `webchat`, `slack`: the terminal summary is delivered on that channel.</ParamField>

<ParamField body="originThreadId" type="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`.
</ParamField>

<ParamField body="callback" type="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`.</ParamField>
<ParamField body="onBehalfOf" type="object">`{ kind: "customer", externalId }`: the run belongs to a customer, who can never be an approver.</ParamField>
<ParamField body="workspace" type="object">`{ repo?, ref?, credentialsRef?, sizeGb? }` overriding a declared [Job-tier workspace](/build/workflows/job-tier); `sizeGb` from 1 to 50.</ParamField>
<ParamField body="initialState" type="object">Seeds the run state; at most 64 KB.</ParamField>

**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**

| Status | Code                                                                                                | Meaning                                                                                                                                                  |
| ------ | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `VALIDATION_FAILED`, `ORIGIN_THREAD_INVALID`, `CALLBACK_THREAD_REQUIRED`, `CORRELATION_KEY_INVALID` | Bad body; an inline definition answers the compose validator's issues, each with a `path`                                                                |
| `402`  | `INSUFFICIENT_CREDITS`                                                                              | The organization cannot pay for the run                                                                                                                  |
| `403`  | `INSUFFICIENT_SCOPE`                                                                                | An inline `graph` or `script` without `workflows:write`                                                                                                  |
| `404`  | `WORKFLOW_NOT_FOUND`                                                                                | No such definition on this agent                                                                                                                         |
| `409`  | `IDEMPOTENCY_KEY_REUSED`, `RUNS_IN_FLIGHT`                                                          | The key is held by another start (`existingRunId`); the workflow's `concurrencyPolicy` is `forbid` and a run is live (`blockingRunId`, `blockingStatus`) |
| `413`  | `PAYLOAD_TOO_LARGE`                                                                                 | `input` or `graph` over 256 KB                                                                                                                           |
| `422`  | `COMPOSE_FORM_FORBIDDEN`                                                                            | An inline `script` under a graph-only policy                                                                                                             |
| `429`  | `CONCURRENCY_CAP`, `SUSPENDED_CAP`                                                                  | The organization's cap is full (`cap`, `active` or `suspended`, `retryAfterSeconds`)                                                                     |
| `501`  | `INLINE_START_UNSUPPORTED`                                                                          | The server cannot validate the inline definition; nothing started, do not retry unchanged                                                                |
| `503`  | `CONTROL_UNAVAILABLE`                                                                               | The control plane did not answer; `ambiguous: true`, retry with the same key                                                                             |

Equivalent: `lua workflows start <workflow> --input @input.json --follow`, `Workflows.start()`.

<CodeGroup>
  ```bash CLI theme={null}
  lua workflows start lead-outreach --input '{"segment":"trial"}' \
    --idempotency-key outreach-2026-09-12 --correlation-key outreach:2026-09-12 --tag nightly
  ```

  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/workflows/<<YOUR_AGENT_ID>>/runs', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer <<YOUR_API_KEY>>',
      'Content-Type': 'application/json',
      'Idempotency-Key': 'outreach-2026-09-12',
    },
    body: JSON.stringify({
      workflowName: 'lead-outreach',
      input: { segment: 'trial' },
      correlationKey: 'outreach:2026-09-12',
      tags: ['nightly'],
    }),
  });
  const started: { runId: string; status: string; idempotentReplay?: boolean } = await response.json();
  console.log(response.status, started.runId, started.status);
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.heylua.ai/workflows/<<YOUR_AGENT_ID>>/runs" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: outreach-2026-09-12" \
    -d '{ "workflowName": "lead-outreach", "input": { "segment": "trial" }, "correlationKey": "outreach:2026-09-12", "tags": ["nightly"] }'
  ```
</CodeGroup>

### POST /workflows/:agentId/runs/batch

Starts many runs of one workflow at once. The body may be at most 2 MB.

<ParamField body="workflowId" type="string" required>The definition.</ParamField>

<ParamField body="items" type="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? }`.
</ParamField>

<ParamField body="mode" type="string" default="gate">`gate` parks items over the concurrency cap; `reject` refuses them.</ParamField>
<ParamField body="budget" type="object">Batch-wide default; an item's own budget wins.</ParamField>
<ParamField body="workflowVersionId" type="string">Pin a version.</ParamField>

**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.

<ParamField query="status" type="string">Comma-separated statuses.</ParamField>
<ParamField query="workflowId" type="string">Only runs of this definition.</ParamField>
<ParamField query="trigger" type="string">One trigger value.</ParamField>
<ParamField query="since" type="integer">Unix milliseconds; runs updated at or after it.</ParamField>
<ParamField query="batchId" type="string">Runs of one batch start.</ParamField>
<ParamField query="correlationKey" type="string">Exact match.</ParamField>
<ParamField query="tag" type="string">Repeatable, at most 10.</ParamField>
<ParamField query="minDurationMs" type="integer">Terminal runs only; `maxDurationMs` likewise.</ParamField>
<ParamField query="sort" type="string" default="-createdAt">`-createdAt`, `createdAt`, `-durationMs`, or `durationMs`.</ParamField>
<ParamField query="cursor" type="string">`nextCursor` from the previous page.</ParamField>
<ParamField query="limit" type="integer" default="50">From 1 to 100.</ParamField>

**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()`.

<CodeGroup>
  ```bash CLI theme={null}
  lua workflows runs --status failed --tag nightly --limit 20
  ```

  ```ts TypeScript theme={null}
  const params = new URLSearchParams({ status: 'failed,timed_out', tag: 'nightly', limit: '20' });
  const response = await fetch(`https://api.heylua.ai/workflows/<<YOUR_AGENT_ID>>/runs?${params}`, {
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>' },
  });
  const page: { items: Array<{ runId: string; status: string; workflowName?: string }>; nextCursor: string | null } =
    await response.json();
  for (const run of page.items) console.log(run.runId, run.status);
  ```

  ```bash cURL theme={null}
  curl -G "https://api.heylua.ai/workflows/<<YOUR_AGENT_ID>>/runs" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    --data-urlencode 'status=failed,timed_out' --data-urlencode 'tag=nightly' --data-urlencode 'limit=20'
  ```
</CodeGroup>

### GET /workflows/:agentId/runs/:runId

Returns one run.

<ParamField query="fields" type="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.</ParamField>

**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()`.

<CodeGroup>
  ```bash CLI theme={null}
  lua workflows status <runId> --steps
  ```

  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/workflows/<<YOUR_AGENT_ID>>/runs/<<RUN_ID>>?fields=full', {
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>' },
  });
  const run: { status: string; output?: unknown; steps: Array<{ stepId: string; status: string }> } = await response.json();
  console.log(run.status, run.steps.map((step) => `${step.stepId}=${step.status}`).join(' '));
  ```

  ```bash cURL theme={null}
  curl "https://api.heylua.ai/workflows/<<YOUR_AGENT_ID>>/runs/<<RUN_ID>>?fields=full" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>"
  ```
</CodeGroup>

### 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.

<ParamField body="mode" type="string" default="request">`request` or `force`.</ParamField>
<ParamField body="reason" type="string">At most 2,000 characters, kept for audit.</ParamField>

**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()`.

<CodeGroup>
  ```bash CLI theme={null}
  lua workflows cancel <runId> --reason "wrong input"
  ```

  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/workflows/<<YOUR_AGENT_ID>>/runs/<<RUN_ID>>/cancel', {
    method: 'POST',
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>', 'Content-Type': 'application/json' },
    body: JSON.stringify({ reason: 'wrong input' }),
  });
  const verdict: { status: string; nextAction: 'cancel_again' | 'force' | 'none'; forceAvailableAt?: number } =
    await response.json();
  console.log(verdict.status, verdict.nextAction);
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.heylua.ai/workflows/<<YOUR_AGENT_ID>>/runs/<<RUN_ID>>/cancel" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    -H "Content-Type: application/json" \
    -d '{ "reason": "wrong input" }'
  ```
</CodeGroup>

### 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.

<ParamField body="note" type="string">At most 2,000 characters.</ParamField>
<ParamField body="resetAttempts" type="boolean">Restart the step's automatic retry budget; the attempt number keeps counting up. Ignored on a gate or billing park.</ParamField>

**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.

<ParamField body="outcome" type="string" default="complete">`skip`, `complete`, or `fail`.</ParamField>
<ParamField body="output" type="any">Required for `complete`, ignored otherwise; at most 256 KB serialized; validated against the step's `outputSchema`.</ParamField>
<ParamField body="note" type="string">At most 2,000 characters.</ParamField>

**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}'`.

<CodeGroup>
  ```bash CLI theme={null}
  lua workflows resolve-step <runId> --step sendEmails --outcome skip --note "Emails were sent by hand"
  ```

  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/workflows/<<YOUR_AGENT_ID>>/runs/<<RUN_ID>>/steps/sendEmails/resolve', {
    method: 'POST',
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>', 'Content-Type': 'application/json' },
    body: JSON.stringify({ outcome: 'skip', note: 'Emails were sent by hand' }),
  });
  const resolved: { resolved: boolean; outcome?: string; runStatus: string } = await response.json();
  console.log(resolved.runStatus);
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.heylua.ai/workflows/<<YOUR_AGENT_ID>>/runs/<<RUN_ID>>/steps/sendEmails/resolve" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    -H "Content-Type: application/json" \
    -d '{ "outcome": "skip", "note": "Emails were sent by hand" }'
  ```
</CodeGroup>

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

Raises the budget of a run parked on a `budget` gate. Creator or organization admin.

<ParamField body="maxCredits" type="number">New credit cap, above 0.</ParamField>
<ParamField body="maxSteps" type="integer">New step cap, at least 1.</ParamField>
<ParamField body="maxJobSeconds" type="integer">New Job-tier seconds cap, at least 1.</ParamField>
<ParamField body="maxDurationSeconds" type="integer">New wall-clock cap, at least 1.</ParamField>
<ParamField body="note" type="string">At most 2,000 characters.</ParamField>

**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.

| Status | Codes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `VALIDATION_FAILED`, `BAD_CURSOR`, `CORRELATION_KEY_INVALID`, `ORIGIN_THREAD_INVALID`, `CALLBACK_THREAD_REQUIRED`, `BATCH_TOO_LARGE`, `BATCH_ITEM_INVALID`, `RESUME_SCHEMA_INVALID`, `SIGNAL_SCHEMA_INVALID`, `PATCH_INVALID`, `RESOLVE_OUTPUT_INVALID`, `CAP_EXCEEDED`, `ARTEFACT_SCHEMA_INVALID`, `WORKFLOW_NOT_ON_AGENT`, `GOAL_MAX_RUNS_INVALID`, `GOAL_RAISE_BELOW_SPENT`, `REASSIGN_SAME_USER`, `ROLLUP_RANGE_TOO_WIDE`                                                                                                                                                    |
| `402`  | `INSUFFICIENT_CREDITS`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `403`  | `INSUFFICIENT_SCOPE`, `NOT_RUN_CREATOR`, `APPROVAL_REQUIRES_HUMAN`, `READ_OUTPUTS_REQUIRED`, `NOT_AN_APPROVER`, `FOUR_EYES_REQUIRED`, `APPROVAL_SURFACE_DENIED`, `STEP_UP_REQUIRED`, `NOT_ITEM_APPROVER`, `CUSTOMER_PRINCIPAL_CANNOT_APPROVE`, `NOT_ORG_ADMIN`                                                                                                                                                                                                                                                                                                                   |
| `404`  | `RUN_NOT_FOUND`, `STEP_NOT_FOUND`, `WORKFLOW_NOT_FOUND`, `AGENT_NOT_FOUND`, `WORKFLOWS_DISABLED`, `NOT_SCRIPT_RUN`, `APPROVAL_NOT_FOUND`, `ITEM_NOT_FOUND`, `CORRELATION_KEY_NOT_FOUND`, `SCHEDULE_NOT_FOUND`, `GOAL_NOT_FOUND`, `ARTEFACT_NOT_FOUND`, `EXPORT_NOT_FOUND`, `WORKSPACE_NOT_FOUND`, `JOB_NOT_FOUND`, `JOB_LOGS_NOT_PERSISTED`, `USER_NOT_IN_ORG`                                                                                                                                                                                                                   |
| `409`  | `IDEMPOTENCY_KEY_REUSED`, `RUNS_IN_FLIGHT`, `RUN_TERMINAL`, `RUN_NOT_TERMINAL`, `NOT_SUSPENDED`, `USE_SIGNAL_ROUTE`, `NOT_RESUMABLE`, `PAYLOAD_MISMATCH`, `EDIT_NOT_ALLOWED`, `PAYLOAD_FINGERPRINT_STALE`, `ITEM_ALREADY_DECIDED`, `CORRELATION_KEY_AMBIGUOUS`, `ORG_PURGING`, `SCHEDULE_CAP`, `GOAL_SCHEDULE`, `GOAL_CAP`, `GOAL_NOT_ACTIVE`, `GOAL_VERSION_CONFLICT`, `GOAL_BUDGET_EXHAUSTED`, `STEP_NOT_PARKED`, `STEP_RETRY_CAP`, `BUDGET_NOT_RAISABLE`, `ARTEFACT_PENDING`, `EXPORT_IN_PROGRESS`, `WORKSPACE_IN_USE`, `NOT_A_WORKFLOW_RUN`, `REASSIGN_TARGET_LACKS_EXECUTE` |
| `413`  | `PAYLOAD_TOO_LARGE`, `OUTPUT_TOO_LARGE`, `ARTEFACT_TOO_LARGE`, `PAYLOAD_PAGE_REQUIRED`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `422`  | `COMPOSE_FORM_FORBIDDEN`, `SIGNAL_SCHEMA_UNCOMPILABLE`, `RESUME_SCHEMA_UNCOMPILABLE`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `423`  | `ORG_ARCHIVED`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `429`  | `RATE_LIMITED`, `CONCURRENCY_CAP`, `SUSPENDED_CAP`, `SSE_SUBSCRIBER_CAP`, `SIGNAL_INBOX_FULL`, `EXPORT_RATE_LIMITED`                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `501`  | `INLINE_START_UNSUPPORTED`, `ERASE_UNAVAILABLE`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `503`  | `CONTROL_UNAVAILABLE`, `UPSTREAM_UNAVAILABLE`, `SCHEDULE_PROVISION_FAILED`, `ARTEFACT_READ_FAILED`                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |

## See also

* [`Workflows`](/reference/sdk/workflows) — the same operations from agent code, and what the deployed runtime serves
* [`lua workflows`](/reference/cli/workflows) — every action, with exit codes 4 to 8 for run outcomes
* [Workflow builder](/reference/sdk/workflow-builder) — the definitions these routes run
* [Operate runs](/build/workflows/operate-runs) — the day-two loop: watch, retry, resolve, export
* [Workflow approvals](/reference/rest/workflow-approvals) — answering the human input a run waits for
