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

# Workflow events

> Step reads, the run event ledger over SSE or polling, the script journal, the chat re-attach stream, artifacts, and evidence exports

These routes read what a workflow [run](/concepts/workflows) did: one step with every attempt, the run's event ledger as a live stream or one page at a time, a script-form run's journal, the chat card's re-attach stream, and the artifacts and evidence bundles a run leaves behind. Every route needs `workflows:read` on the agent; payloads additionally need `workflows:read-outputs`. Starting, listing, and recovering runs are on [Workflows](/reference/rest/workflows), which also holds the [error codes](/reference/rest/workflows#error-codes) and the base URL, scopes, and envelope pointer; human input is on [Workflow approvals](/reference/rest/workflow-approvals).

*Verified against lua-cli 3.33.0.*

## Steps

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

Returns one step with every attempt.

<ParamField query="attempt" type="integer">One attempt instead of the current one; a non-number is `400 VALIDATION_FAILED`.</ParamField>

**Response**

`200` with the step: its summary fields (`stepId`, `kind`, `label`, `status`, `attempt`, `maxAttempts`, `retriesRemaining`, `dependsOn`, timings, `error`, `suspend`, `park`, `artefactIds`, the `job` block on Job-tier rows) plus `input`, `output`, `outputRef`, and `attempts[]`. Payloads are `{ "__hidden": true }` without `workflows:read-outputs`. `404 STEP_NOT_FOUND` or `RUN_NOT_FOUND`.

Equivalent: `lua workflows status <runId> --steps`.

## Events

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

Streams the run's event ledger, or returns one page of it.

<ParamField header="Accept" type="string">`text/event-stream` opens the stream; anything else answers one JSON page.</ParamField>
<ParamField header="Last-Event-ID" type="string">Resume after this sequence number on reconnect; wins over `afterSeq`.</ParamField>
<ParamField query="afterSeq" type="integer">Start after this sequence number.</ParamField>
<ParamField query="types" type="string">Comma-separated event types to keep, in the replay and the tail.</ParamField>
<ParamField query="limit" type="integer" default="200">Polling form only; at most 500 events per page.</ParamField>

**Response**

The stream replays missed history from your cursor, then tails live events. Each ledger event is one frame, `id: <seq>`, `event: <type>`, `data: <event>`, where the event is `{ runId, seq, ts, type, stepId?, attempt?, actor: { kind, id? }, data?, orgId, agentId, userId? }` with `seq` strictly increasing per run, `ts` an ISO 8601 timestamp, `actor.kind` one of `engine`, `worker`, `sweeper`, `runner`, `user`, `agent`, `webhook`, `api`, `schedule`, or `system`, and `data` at most 8 KB. Control frames carry no id: `heartbeat { ts, head }` every 15 seconds, `end { seq, status }` after the terminal event (the server then closes), `reconnect { reason }`, `error { code }` with `run_gone`, `forbidden`, or `internal`, and `workflow.output.acl_changed { restricted, ts }` when your output visibility flipped mid-stream. Without `workflows:read-outputs` each frame arrives without `data` and with `restricted: true`; the read scope and the output grant are re-checked every 60 seconds, and a lost read scope ends the stream with `error { code: "forbidden" }`. The event vocabulary is `run.*`, `step.*`, `subrun.*`, `approval.*`, `signal.*`, `timer.*`, `billing.*`, `journal.*`, `goal.*`, `schedule.*`, and `connection.*`; the terminal members are `run.completed`, `run.failed`, `run.cancelled`, `run.abandoned`, and `run.timed_out`, and a version migration appends `run.migration_scheduled`, `run.migration_applied`, `run.migration_refused`, or `run.migrated`.

The polling form answers `200` with `{ "events": [...] }` under the same cursor rules.

**Errors**

`404 RUN_NOT_FOUND`; `429 SSE_SUBSCRIBER_CAP` when too many streams are open on the run.

Equivalent: `lua workflows watch <runId>` (stream) and `lua workflows logs <runId>` (polling).

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

  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/workflows/<<YOUR_AGENT_ID>>/runs/<<RUN_ID>>/events?afterSeq=0&limit=500', {
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>' },
  });
  const { events }: { events: Array<{ seq: number; type: string; stepId?: string; ts: string }> } = await response.json();
  for (const event of events) console.log(event.seq, event.type, event.stepId ?? '');
  ```

  ```bash cURL theme={null}
  curl -N "https://api.heylua.ai/workflows/<<YOUR_AGENT_ID>>/runs/<<RUN_ID>>/events?afterSeq=0" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    -H "Accept: text/event-stream"
  ```
</CodeGroup>

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

Reads a script-form run's journal.

<ParamField query="afterSeq" type="integer">Start after this entry; a negative or non-numeric value is `400 VALIDATION_FAILED`.</ParamField>
<ParamField query="limit" type="integer">Page size.</ParamField>
<ParamField query="includePayloads" type="boolean">`true` or `1` includes entry payloads; needs `workflows:read-outputs`.</ParamField>

**Response**

`200` with `{ "items", "nextAfterSeq", "outputsHidden"? }`, where `outputsHidden: "read-outputs"` marks a page served without the grant. `403 READ_OUTPUTS_REQUIRED`; `404 RUN_NOT_FOUND` or `NOT_SCRIPT_RUN`.

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

Streams the run as chat parts (`event: part` frames whose `data` is `{ type: "data-lua-workflow-run" | "data-lua-workflow-step", id, data }`) for a chat card that re-attaches to a live run, with the same `heartbeat`, `end`, `reconnect`, and `error` control frames and `Last-Event-ID` rule as the events stream; a terminal run answers one part and `end`.

<ParamField query="afterSeq" type="integer">Start after this sequence number.</ParamField>
<ParamField query="threadId" type="string">Echoed into the audit record; never used for access checks.</ParamField>

**Errors**

`404 RUN_NOT_FOUND`, also when the stream is disabled on the server; `409 NOT_A_WORKFLOW_RUN` for a cloud-task id; `429 SSE_SUBSCRIBER_CAP`.

## Artifacts and exports

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

Presigns a download of a run artifact, or of an offloaded step or run output (`{ "__cdnRef" }` or `{ "__datasetRef" }`).

<ParamField query="offset" type="integer">Dataset refs only: first row of the page, from 0.</ParamField>
<ParamField query="limit" type="integer" default="100">Dataset refs only: rows per page, at most 1,000.</ParamField>

**Response**

`200` with `{ "artefactId", "kind", "url", "expiresAt", "contentType", "bytes" }`; a file adds `sha256`, and a dataset ref adds `rowCount`, `schema`, and `page { rows, offset, limit, nextOffset }`. The link expires after 300 seconds. `400 VALIDATION_FAILED` for an `offset` past the last row; `403 READ_OUTPUTS_REQUIRED`; `404 ARTEFACT_NOT_FOUND` or `RUN_NOT_FOUND`; `409 ARTEFACT_PENDING`; `503 ARTEFACT_READ_FAILED`.

### POST /workflows/:agentId/artefacts

Declares an input artifact to upload, for example evidence to attach to an approval. Needs `workflows:execute`.

<ParamField body="filename" type="string" required>At most 255 characters.</ParamField>
<ParamField body="contentType" type="string" required>At most 255 characters.</ParamField>
<ParamField body="bytes" type="integer" required>Declared size, at most 256 MB.</ParamField>
<ParamField body="kind" type="string">`file`, `dataset`, `image`, or `document`.</ParamField>
<ParamField body="title" type="string">At most 200 characters.</ParamField>
<ParamField body="datasetSchema" type="object">`kind: "dataset"` only.</ParamField>

**Response**

`201` with `{ "artefactId", "uploadUrl", "expiresAt" }`; the row stays `pending` until the bytes land, and an unclaimed declaration expires after 24 hours. `400 ARTEFACT_SCHEMA_INVALID` (a `datasetSchema` on another kind, or not an object) or `VALIDATION_FAILED`; `413 ARTEFACT_TOO_LARGE`.

### PUT /workflows/:agentId/artefacts/:artefactId/content

Uploads the bytes of a declared artifact. Send the raw body with its `Content-Type`; it is never parsed as JSON. Needs `workflows:execute`.

**Response**

`200` with `{ "artefactId", "status": "ready", "bytes", "sha256" }`. `404 ARTEFACT_NOT_FOUND` for an unknown, foreign, or expired declaration; `413 ARTEFACT_TOO_LARGE` past the declared size or 256 MB.

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

Requests an evidence bundle of a terminal run. Needs `workflows:read-outputs`.

<ParamField body="includeInputs" type="boolean">Run and step inputs; on a customer's run this also needs `workflows:write`.</ParamField>
<ParamField body="includeArtefacts" type="boolean" default="true">The artifact manifest.</ParamField>
<ParamField body="includeJournal" type="boolean" default="true">The script-run journal.</ParamField>

**Response**

`202` with `{ "exportId", "status": "pending" }`. `403 READ_OUTPUTS_REQUIRED`; `404 RUN_NOT_FOUND`; `409 RUN_NOT_TERMINAL` or `EXPORT_IN_PROGRESS` (`exportId`); `429 EXPORT_RATE_LIMITED` past 5 exports of one run per day.

Equivalent: `lua workflows archive-runs --since 8d --out ./archive`.

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

Reads an export's status and download link. Needs `workflows:read-outputs`.

**Response**

`200` with `{ "exportId", "runId", "status": "pending" | "ready" | "failed" | "expired" | "pushed", "url"?, "expiresAt"?, "bytes"?, "manifest": { files, sha256 }?, "error"?, "createdAt" }`; a bundle can be downloaded for 7 days, after which it reads `expired`. `403 READ_OUTPUTS_REQUIRED`; `404 RUN_NOT_FOUND` or `EXPORT_NOT_FOUND`.

## See also

* [Workflows](/reference/rest/workflows) — the run model, run routes, and every error code
* [Workflow approvals](/reference/rest/workflow-approvals) — the routes that answer the events a run waits on
* [`lua workflows`](/reference/cli/workflows) — `watch`, `logs`, and `archive-runs` over the same routes
* [`Workflows`](/reference/sdk/workflows) — reading a run from agent code
* [Operate runs](/build/workflows/operate-runs) — watching, exporting, and erasing runs in practice
