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

# Run lifecycle and event consumption

> Run state machine, event streaming, polling, and awaiting_input patterns.

# Run Lifecycle and Event Consumption

How runs progress through their lifecycle, how to consume events, and how to handle the awaiting\_input (human-in-the-loop) pattern. This document provides the behavioral context needed to build a client that correctly handles all run states.

***

## Run state machine

```
                         ┌──────────────┐
           POST /v1/runs │              │
          ──────────────►│   queued     │
                         │              │
                         └──────┬───────┘
                                │ worker picks up
                                ▼
                         ┌──────────────┐
                    ┌───►│              │◄──────────────────────────┐
                    │    │   running    │                           │
                    │    │              │                           │
                    │    └──┬───┬───┬───┘                           │
                    │       │   │   │                               │
                    │       │   │   │ heartbeat lost                │
                    │       │   │   ▼                               │
                    │       │   │ ┌──────────────┐  POST .../resume │
                    │       │   │ │   stalled    │─────────────────►┘
                    │       │   │ └──────────────┘
                    │       │   │
                    │       │   │ judge: await_input
                    │       │   ▼
                    │       │ ┌──────────────────┐
                    │       │ │ awaiting_input   │ (sub-state of running)
                    │       │ │                  │
                    │       │ └──┬───┬───────────┘
                    │       │    │   │
                    │       │    │   │ signal: approve / submit_input
                    │       │    │   └──────────────────────────────►┐
                    │       │    │                                   │
                    │       │    │ signal: reject                    │
                    │       │    │ OR timeout                        │ back to running
                    │       │    ▼                                   │
                    │       │  ┌──────────┐                         │
                    │       │  │  failed  │                         │
                    │       │  └──────────┘                         │
                    │       │                                       │
   POST .../retry   │       │ execution completes                   │
   (from failed)    │       ▼                                       │
                    │    ┌────────────┐                             │
                    │    │ succeeded  │                             │
                    │    └────────────┘                             │
                    │                                              │
                    │    ┌────────────┐                             │
                    └────│  POST      │                             │
                         │  .../retry │                             │
                         └────────────┘                             │
                                                                   │
                         ┌────────────┐                             │
        POST .../cancel  │ cancelled  │                             │
        (any non-terminal)└───────────┘                             │
```

### Status definitions

| Status      | Terminal | Meaning                                                                                                                 | Client action                                                                                                                                                                                                                                                                                             |
| ----------- | :------: | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `queued`    |    No    | Accepted, waiting for worker                                                                                            | Wait. Connect SSE or start polling.                                                                                                                                                                                                                                                                       |
| `running`   |    No    | Worker is actively executing steps                                                                                      | Listen for `step.progress` and `step.done` events.                                                                                                                                                                                                                                                        |
| `stalled`   |    No    | Worker heartbeat lost or timed out                                                                                      | Call `POST /v1/runs/{id}/resume` or wait for auto-recovery.                                                                                                                                                                                                                                               |
| `succeeded` |    Yes   | Run completed successfully                                                                                              | Read final events for output content.                                                                                                                                                                                                                                                                     |
| `failed`    |    Yes   | Run terminated with an error. Includes infrastructure hard-limit termination (cost ceiling or duration limit exceeded). | Check `reason_code` in the terminal event (e.g. `run.worker.failed`). If you see **`run.limit_exceeded`**, the run hit a server-enforced limit. Optionally call `POST /v1/runs/{id}/retry` for transient failures; limit-exceeded runs should not be retried as-is without reducing scope or consumption. |
| `cancelled` |    Yes   | Cancelled by client request                                                                                             | No further events will be produced.                                                                                                                                                                                                                                                                       |

### awaiting\_input (sub-state)

When the orchestrator's judge determines that external input is needed, the run enters the `awaiting_input` sub-state. The run `status` remains `running` in the API response, but a `run.awaiting_input` event is emitted.

Client responsibilities:

1. Detect the `run.awaiting_input` event in your SSE stream or polling loop.
2. Present the `reason_code` and `input_kind` to the user.
3. Call `POST /v1/runs/{id}/signal` with the appropriate action.
4. If no signal is sent within the server timeout, the run escalates (fails or replans).

### Hard limit termination

The server enforces per-run resource limits. Runs that exceed either limit are terminated automatically:

* **Cost ceiling:** Accumulated token consumption for the run exceeds the server's per-run token budget.
* **Duration limit:** Wall-clock time since run creation exceeds the server's per-run duration limit.

When a limit is reached:

1. The run transitions to `failed`.
2. A **`run.limit_exceeded`** event is appended, visible in `GET /v1/runs/{id}/events`.
3. No further steps execute.

**Event payload** (inside the standard event envelope; field names match the wire shape your client parses):

| Field          | Type   | Description                                                             |
| -------------- | ------ | ----------------------------------------------------------------------- |
| `limitType`    | string | `cost_ceiling` or `duration_limit`                                      |
| `currentValue` | number | The value that triggered the limit (tokens consumed or seconds elapsed) |
| `threshold`    | number | The configured limit                                                    |
| `unit`         | string | `tokens` or `seconds`                                                   |

**Example** (illustrative; exact nesting follows your `RunEvent` / `payload.value` convention):

```json  theme={null}
{
  "seq": 42,
  "type": "run.limit_exceeded",
  "timestamp": "2026-03-25T14:30:00.000Z",
  "payload": {
    "redacted": false,
    "value": {
      "limitType": "cost_ceiling",
      "currentValue": 2100000,
      "threshold": 2000000,
      "unit": "tokens"
    }
  }
}
```

**Client handling:**

* Treat **`run.limit_exceeded`** as a terminal failure.
* Do not retry the same run expecting success — repeat execution will hit the same limit.
* Reduce task scope or token consumption before creating a new run.
* Include **`run.limit_exceeded`** in terminal event handling alongside **`run.worker.failed`** and **`run.worker.succeeded`**.

***

## Event consumption patterns

### Pattern 1: SSE streaming (recommended)

Real-time event delivery. Best for interactive UIs.

```typescript  theme={null}
import { fetchEventSource } from "@microsoft/fetch-event-source";

interface StreamOptions {
  apiBase: string;
  apiKey: string;
  runId: string;
  cursor?: number;
  onEvent: (event: RunEvent) => void;
  onComplete: () => void;
  onError: (err: Error) => void;
}

async function streamRunEvents(opts: StreamOptions): Promise<void> {
  const url = new URL(`${opts.apiBase}/v1/runs/${opts.runId}/events/stream`);
  if (opts.cursor !== undefined) {
    url.searchParams.set("cursor", String(opts.cursor));
  }

  await fetchEventSource(url.toString(), {
    headers: {
      Authorization: `Bearer ${opts.apiKey}`,
    },
    onmessage(msg) {
      if (msg.event !== "run_event") return;
      const event: RunEvent = JSON.parse(msg.data);
      opts.onEvent(event);

      const terminalTypes = [
        "run.worker.succeeded",
        "run.worker.failed",
        "run.cancelled",
        "run.limit_exceeded",
      ];
      if (terminalTypes.includes(event.type)) {
        opts.onComplete();
      }
    },
    onerror(err) {
      opts.onError(err instanceof Error ? err : new Error(String(err)));
    },
  });
}
```

**Reconnection**: If the connection drops, reconnect with `cursor` set to the `seq` of the last event you received. The server will resume from that point. Events are idempotent — receiving the same `seq` twice is safe; deduplicate by `seq` on the client.

### Pattern 2: Cursor-based polling (fallback)

For environments without SSE support or when you need simple retry semantics.

```typescript  theme={null}
interface PollOptions {
  apiBase: string;
  apiKey: string;
  runId: string;
  pollIntervalMs?: number;
  onEvent: (event: RunEvent) => void;
}

async function pollRunEvents(opts: PollOptions): Promise<void> {
  const interval = opts.pollIntervalMs ?? 1000;
  let cursor: number | undefined;

  while (true) {
    const params = new URLSearchParams();
    if (cursor !== undefined) params.set("cursor", String(cursor));
    params.set("limit", "100");

    const res = await fetch(
      `${opts.apiBase}/v1/runs/${opts.runId}/events?${params}`,
      { headers: { Authorization: `Bearer ${opts.apiKey}` } },
    );

    if (!res.ok) {
      throw new Error(`Poll failed: ${res.status}`);
    }

    const body = await res.json();

    for (const event of body.events) {
      opts.onEvent(event);
    }

    // Check for terminal event
    const terminal = body.events.find(
      (e: RunEvent) =>
        e.type === "run.worker.succeeded" ||
        e.type === "run.worker.failed" ||
        e.type === "run.cancelled" ||
        e.type === "run.limit_exceeded",
    );
    if (terminal) break;

    cursor = body.next_cursor;
    await new Promise((r) => setTimeout(r, interval));
  }
}
```

### Pattern 3: Simple status polling (minimal)

If you only need to know when the run finishes and don't need intermediate events:

```typescript  theme={null}
async function waitForCompletion(
  apiBase: string,
  apiKey: string,
  runId: string,
  pollIntervalMs = 2000,
): Promise<{ status: string; id: string }> {
  const terminalStatuses = ["succeeded", "failed", "cancelled"];

  while (true) {
    const res = await fetch(`${apiBase}/v1/runs/${runId}`, {
      headers: { Authorization: `Bearer ${apiKey}` },
    });
    const run = await res.json();

    if (terminalStatuses.includes(run.status)) {
      return run;
    }

    await new Promise((r) => setTimeout(r, pollIntervalMs));
  }
}
```

***

## Typical event sequence

For field-level payload documentation and example JSON for each event type, see the [Events reference](/guides/events).

A successful run produces events roughly in this order:

```
1. run.created              — Run accepted
2. run.worker.started       — Worker picked up (queued → running)
3. step.progress (×N)       — Streaming content deltas and tool calls
4. run.tool.invoked (×N)    — Tool execution results
5. step.done                — Step completed with full content
6. run.coordination.decision — Judge evaluates: continue / replan / stop
   (Steps 3–6 repeat for each execution cycle)
7. run.worker.succeeded     — Run completed (terminal)
```

### With awaiting\_input:

```
1. run.created
2. run.worker.started
3. step.progress (×N)
4. step.done
5. run.coordination.decision  — decision_type: "await_input"
6. run.awaiting_input          — Run paused, waiting for signal
   (Client sends POST /v1/runs/{id}/signal)
7. run.signal_applied          — Signal processed
   OR run.input_received       — Input submitted
8. step.progress (×N)          — Execution resumes
9. step.done
10. run.worker.succeeded
```

### With retry:

```
1. run.created
2. run.worker.started
3. run.worker.failed           — Run failed
   (Client calls POST /v1/runs/{id}/retry)
4. run.worker.retry_scheduled  — Re-enqueued
5. run.worker.started          — Worker picks up again
6. step.progress (×N)
7. run.worker.succeeded
```

***

## Building a run viewer UI

A client displaying run progress typically needs these UI states:

| Run status                                      | SSE state                                 | UI rendering                                |
| ----------------------------------------------- | ----------------------------------------- | ------------------------------------------- |
| `queued`                                        | Not connected yet or connected, no events | "Waiting for worker..." spinner             |
| `running` + `step.progress` (`content_delta`)   | Receiving events                          | Stream text content in real time            |
| `running` + `step.progress` (`tool_call_start`) | Receiving events                          | Show "Using tool: {tool_name}..."           |
| `running` + `run.tool.invoked`                  | Receiving events                          | Show tool result summary                    |
| `running` + `step.done`                         | Receiving events                          | Display completed step content              |
| `running` + `run.coordination.decision`         | Receiving events                          | Show orchestration decision (optional)      |
| `running` + `run.awaiting_input`                | Receiving events                          | **Show approval/input UI**                  |
| `stalled`                                       | Stream may have closed                    | Show "Run stalled" with resume button       |
| `succeeded`                                     | Stream closed                             | Show final content, success state           |
| `failed`                                        | Stream closed                             | Show error with `reason_code`, retry button |
| `cancelled`                                     | Stream closed                             | Show cancelled state                        |

### Rendering step.progress events

`step.progress` events with `kind: "content_delta"` contain incremental text in `content_delta`. Append these to build streaming text output:

```typescript  theme={null}
let currentContent = "";

function handleStepProgress(event: RunEvent) {
  const val = event.payload.value;
  switch (val.kind) {
    case "content_delta":
      currentContent += val.content_delta ?? "";
      renderStreamingText(currentContent);
      break;
    case "tool_call_start":
      renderToolStart(val.tool_name!);
      break;
    case "tool_call_done":
      renderToolDone(val.tool_call_id!);
      break;
  }
}
```

When `step.done` arrives, `content` contains the full text for that step — use it to replace the streamed content and ensure consistency.

***

## Error handling

### Transient errors

* **SSE disconnects**: Reconnect with `cursor` = last `seq`. The server resumes from there.
* **HTTP 500 / 502 / 503**: Retry with exponential backoff (start at 1s, max 30s).
* **HTTP 429**: Respect `Retry-After` header if present; otherwise back off.

### Permanent errors

* **HTTP 401**: API key is invalid, revoked, or expired. Do not retry — re-authenticate.
* **HTTP 403**: Key does not have access to this resource. Do not retry.
* **HTTP 404**: Run does not exist or belongs to a different customer. Do not retry.

### Run failures

When a run reaches `failed` status:

1. Check the `reason_code` in the `run.worker.failed` event payload when present, or a preceding **`run.limit_exceeded`** event for hard-limit termination.
2. If the failure is transient (e.g. provider timeout), retry with `POST /v1/runs/{id}/retry`.
3. If the failure is permanent (e.g. policy denial, invalid input, **`run.limit_exceeded`**), fix the issue or reduce scope and create a new run.

***

## TypeScript types

Suggested type definitions for your client:

```typescript  theme={null}
interface RunEvent {
  seq: number;
  type: string;
  timestamp: string;
  payload: {
    redacted: boolean;
    value: Record<string, unknown>;
  };
}

interface Run {
  id: string;
  workspace_id: string | null;
  subject_id: string | null;
  status: "queued" | "running" | "succeeded" | "failed" | "cancelled" | "stalled";
  run_class: "default" | "short" | "long";
  metadata: {
    created_at: string;
    updated_at: string;
  };
  event_payload: {
    redacted: boolean;
    value: unknown | null;
  };
  request_id: string;
}

interface CreateRunRequest {
  input: Record<string, unknown>;
  metadata: Record<string, unknown>;
  workspace_id?: string;
  subject_id?: string;
  attachment_refs?: string[];
  sensitivity_tags?: SensitivityTag[];
  routing_hint?: "skip_planning" | "require_planning";
  run_class?: "default" | "short" | "long";
  streaming?: boolean;
  plan?: Record<string, unknown>;
}

interface SensitivityTag {
  path: string;
  classification: "pii" | "financial" | "health" | "credential" | "proprietary";
  redaction?: "mask" | "drop";
  retention_hint_days?: number;
}

interface SignalRequest {
  action: "approve" | "reject" | "submit_input";
  payload?: unknown;
  idempotency_key?: string;
}

interface EventsResponse {
  events: RunEvent[];
  next_cursor: number;
  request_id: string;
}

interface ErrorResponse {
  error: string;
  reason_code: string;
  request_id: string;
}
```


Built with [Mintlify](https://mintlify.com).