---
title: Events
description: Subscribe to fine-grained lifecycle events emitted during workflow execution.
---

Smithers emits typed `SmithersEvent` objects throughout a run. Subscribe via `onProgress` in `runWorkflow`, or read persisted events from NDJSON log files.

Events serve as the durable replay/audit log, correlate with structured logs through `runId`/`nodeId`/`attempt`, and drive built-in lifecycle counters. For OTLP export and Prometheus/Grafana setup, see [Observability](/guides/monitoring-logs).

## Subscribing

### onProgress Callback

```ts
import { runWorkflow } from "smithers-orchestrator";
import workflow from "./workflow";

await runWorkflow(workflow, {
  input: { description: "Fix bug" },
  onProgress: (event) => {
    console.log(`[${event.type}] at ${event.timestampMs}`);

    if (event.type === "NodeStarted") {
      console.log(`  node: ${event.nodeId}, attempt: ${event.attempt}`);
    }

    if (event.type === "NodeFailed") {
      console.error(`  node: ${event.nodeId}, error:`, event.error);
    }
  },
});
```

### NDJSON Log Files

Events are appended as JSON lines to:

```
.smithers/executions/<runId>/logs/stream.ndjson
```

```bash
# Watch events in real time
tail -f .smithers/executions/abc123/logs/stream.ndjson | jq .

# Filter for failures
cat .smithers/executions/abc123/logs/stream.ndjson | jq 'select(.type == "NodeFailed")'

# Count events by type
cat .smithers/executions/abc123/logs/stream.ndjson | jq -r .type | sort | uniq -c | sort -rn
```

Configure with `logDir` in `runWorkflow` or `--log-dir` / `--no-log` in the CLI.

## Event-Driven Metrics

| Event | Metric |
|---|---|
| `RunStarted` | `smithers.runs.total` |
| `NodeStarted` | `smithers.nodes.started` |
| `NodeFinished` | `smithers.nodes.finished` |
| `NodeFailed` | `smithers.nodes.failed` |
| Approval events | Approval counters |

`trackSmithersEvent` from `smithers-orchestrator/observability` exposes this mapping for custom integrations.

## Common Fields

Every `SmithersEvent`:

| Field | Type | Description |
|---|---|---|
| `type` | `string` | Event type discriminator. |
| `runId` | `string` | Run this event belongs to. |
| `timestampMs` | `number` | Unix timestamp in milliseconds. |

Node-scoped events add:

| Field | Type | Description |
|---|---|---|
| `nodeId` | `string` | Task node ID. |
| `iteration` | `number` | Loop iteration number. |

Attempt-scoped events add:

| Field | Type | Description |
|---|---|---|
| `attempt` | `number` | Attempt number (starts at 1). |

## Event Types

### Supervisor

#### SupervisorStarted

Emitted when the supervisor process starts polling for stale runs.

```ts
{
  type: "SupervisorStarted",
  runId: string,
  pollIntervalMs: number,
  staleThresholdMs: number,
  timestampMs: number,
}
```

`pollIntervalMs`: How often the supervisor checks for stale runs. `staleThresholdMs`: Age after which a run is considered stale.

#### SupervisorPollCompleted

Emitted after each supervisor poll cycle.

```ts
{
  type: "SupervisorPollCompleted",
  runId: string,
  staleCount: number,
  resumedCount: number,
  skippedCount: number,
  durationMs: number,
  timestampMs: number,
}
```

`staleCount`: Runs found to be stale. `resumedCount`: Runs successfully auto-resumed. `skippedCount`: Stale runs skipped (e.g. process still alive). `durationMs`: Wall time for this poll cycle.

### Run Lifecycle

#### RunStarted

Emitted once at the beginning of every run (including resumes).

```ts
{ type: "RunStarted", runId: string, timestampMs: number }
```

#### RunStatusChanged

```ts
{ type: "RunStatusChanged", runId: string, status: RunStatus, timestampMs: number }
```

`RunStatus`: `"running"` | `"waiting-approval"` | `"waiting-event"` | `"waiting-timer"` | `"finished"` | `"continued"` | `"failed"` | `"cancelled"`.

#### RunFinished

```ts
{ type: "RunFinished", runId: string, timestampMs: number }
```

#### RunFailed

```ts
{ type: "RunFailed", runId: string, error: unknown, timestampMs: number }
```

#### RunCancelled

```ts
{ type: "RunCancelled", runId: string, timestampMs: number }
```

#### RunAutoResumed

Emitted by the supervisor when a stale run is automatically restarted.

```ts
{
  type: "RunAutoResumed",
  runId: string,
  lastHeartbeatAtMs: number | null,
  staleDurationMs: number,
  timestampMs: number,
}
```

`lastHeartbeatAtMs`: Unix ms of the last recorded heartbeat, or `null` if no heartbeat was recorded. `staleDurationMs`: How long the run had been stale before resumption.

#### RunAutoResumeSkipped

Emitted when the supervisor decided not to resume a stale run.

```ts
{
  type: "RunAutoResumeSkipped",
  runId: string,
  reason: "pid-alive" | "missing-workflow" | "rate-limited",
  timestampMs: number,
}
```

`reason`: `"pid-alive"` — the original process is still running; `"missing-workflow"` — workflow file could not be located; `"rate-limited"` — resumption was throttled.

#### RunContinuedAsNew

Emitted when a long-running workflow continues as a fresh run, carrying forward state.

```ts
{
  type: "RunContinuedAsNew",
  runId: string,
  newRunId: string,
  iteration: number,
  carriedStateSize: number,
  ancestryDepth?: number,
  timestampMs: number,
}
```

`newRunId`: The run ID of the continuation. `carriedStateSize`: Byte size of the state passed to the new run. `ancestryDepth`: How many continuation hops have occurred (omitted on first continuation).

#### RunForked

Emitted when a run is forked from a parent run's snapshot for time-travel or branching.

```ts
{
  type: "RunForked",
  runId: string,
  parentRunId: string,
  parentFrameNo: number,
  branchLabel?: string,
  timestampMs: number,
}
```

`parentRunId`: The run this fork originated from. `parentFrameNo`: Frame number in the parent run where the fork was taken. `branchLabel`: Optional human-readable label for the branch.

#### ReplayStarted

Emitted when a run begins replaying from a parent run's snapshot.

```ts
{
  type: "ReplayStarted",
  runId: string,
  parentRunId: string,
  parentFrameNo: number,
  restoreVcs: boolean,
  timestampMs: number,
}
```

`parentRunId`: The run being replayed from. `parentFrameNo`: Snapshot frame to replay from. `restoreVcs`: Whether VCS state was restored as part of the replay.

### Frame Events

#### FrameCommitted

Emitted each time the engine renders a new frame.

```ts
{
  type: "FrameCommitted",
  runId: string,
  frameNo: number,
  xmlHash: string,
  timestampMs: number,
}
```

`xmlHash`: SHA-256 hex digest of the canonicalized XML tree.

### Snapshot

#### SnapshotCaptured

Emitted when the engine captures a point-in-time snapshot of the workflow frame, enabling time-travel and forking.

```ts
{
  type: "SnapshotCaptured",
  runId: string,
  frameNo: number,
  contentHash: string,
  timestampMs: number,
}
```

`frameNo`: The frame this snapshot was taken at. `contentHash`: Hash of the snapshot content, used to detect duplicate snapshots.

### Node Lifecycle

#### NodePending

Task identified, waiting to be scheduled.

```ts
{ type: "NodePending", runId: string, nodeId: string, iteration: number, timestampMs: number }
```

#### NodeStarted

```ts
{
  type: "NodeStarted",
  runId: string,
  nodeId: string,
  iteration: number,
  attempt: number,
  timestampMs: number,
}
```

#### NodeFinished

```ts
{
  type: "NodeFinished",
  runId: string,
  nodeId: string,
  iteration: number,
  attempt: number,
  timestampMs: number,
}
```

#### NodeFailed

```ts
{
  type: "NodeFailed",
  runId: string,
  nodeId: string,
  iteration: number,
  attempt: number,
  error: unknown,
  timestampMs: number,
}
```

#### NodeCancelled

```ts
{
  type: "NodeCancelled",
  runId: string,
  nodeId: string,
  iteration: number,
  attempt?: number,
  reason?: string,
  timestampMs: number,
}
```

`reason` may be `"unmounted"` if the task disappeared from the tree after re-render.

#### NodeSkipped

```ts
{ type: "NodeSkipped", runId: string, nodeId: string, iteration: number, timestampMs: number }
```

#### NodeRetrying

Fires before the next attempt starts.

```ts
{
  type: "NodeRetrying",
  runId: string,
  nodeId: string,
  iteration: number,
  attempt: number,
  timestampMs: number,
}
```

`attempt` is the upcoming attempt number.

#### NodeWaitingApproval

```ts
{
  type: "NodeWaitingApproval",
  runId: string,
  nodeId: string,
  iteration: number,
  timestampMs: number,
}
```

#### NodeWaitingTimer

Emitted when a node is suspended waiting for a timer to fire.

```ts
{
  type: "NodeWaitingTimer",
  runId: string,
  nodeId: string,
  iteration: number,
  firesAtMs: number,
  timestampMs: number,
}
```

`firesAtMs`: Unix ms when the timer is scheduled to fire.

### Approval Events

#### ApprovalRequested

```ts
{
  type: "ApprovalRequested",
  runId: string,
  nodeId: string,
  iteration: number,
  timestampMs: number,
}
```

#### ApprovalGranted

```ts
{
  type: "ApprovalGranted",
  runId: string,
  nodeId: string,
  iteration: number,
  timestampMs: number,
}
```

#### ApprovalAutoApproved

Emitted when an approval is granted automatically by a configured policy without human intervention.

```ts
{
  type: "ApprovalAutoApproved",
  runId: string,
  nodeId: string,
  iteration: number,
  timestampMs: number,
}
```

#### ApprovalDenied

```ts
{
  type: "ApprovalDenied",
  runId: string,
  nodeId: string,
  iteration: number,
  timestampMs: number,
}
```

### Tool Events

#### ToolCallStarted

```ts
{
  type: "ToolCallStarted",
  runId: string,
  nodeId: string,
  iteration: number,
  attempt: number,
  toolName: string,
  seq: number,
  timestampMs: number,
}
```

`seq`: sequential counter for tool calls within the attempt.

#### ToolCallFinished

```ts
{
  type: "ToolCallFinished",
  runId: string,
  nodeId: string,
  iteration: number,
  attempt: number,
  toolName: string,
  seq: number,
  status: "success" | "error",
  timestampMs: number,
}
```

### Output Events

#### NodeOutput

Streaming text from an agent.

```ts
{
  type: "NodeOutput",
  runId: string,
  nodeId: string,
  iteration: number,
  attempt: number,
  text: string,
  stream: "stdout" | "stderr",
  timestampMs: number,
}
```

### Timer Events

#### TimerCreated

Emitted when a durable timer is registered with the engine.

```ts
{
  type: "TimerCreated",
  runId: string,
  timerId: string,
  firesAtMs: number,
  timerType: "duration" | "absolute",
  timestampMs: number,
}
```

`timerId`: Stable identifier for this timer. `firesAtMs`: Unix ms when the timer will fire. `timerType`: `"duration"` — created from a relative delay; `"absolute"` — created from a specific wall-clock time.

#### TimerFired

Emitted when a timer fires and resumes its waiting node.

```ts
{
  type: "TimerFired",
  runId: string,
  timerId: string,
  firesAtMs: number,
  firedAtMs: number,
  delayMs: number,
  timestampMs: number,
}
```

`firesAtMs`: Scheduled fire time. `firedAtMs`: Actual fire time. `delayMs`: Difference between actual and scheduled fire time; non-zero indicates scheduler lag.

#### TimerCancelled

Emitted when a timer is cancelled before it fires.

```ts
{
  type: "TimerCancelled",
  runId: string,
  timerId: string,
  timestampMs: number,
}
```

### Task Heartbeat Events

#### TaskHeartbeat

Emitted periodically by long-running tasks to signal they are still alive.

```ts
{
  type: "TaskHeartbeat",
  runId: string,
  nodeId: string,
  iteration: number,
  attempt: number,
  hasData: boolean,
  dataSizeBytes: number,
  intervalMs?: number,
  timestampMs: number,
}
```

`hasData`: Whether the heartbeat carries a checkpoint payload. `dataSizeBytes`: Byte size of any checkpoint data. `intervalMs`: Configured heartbeat interval, if set.

#### TaskHeartbeatTimeout

Emitted when a task fails to send a heartbeat within its configured timeout window.

```ts
{
  type: "TaskHeartbeatTimeout",
  runId: string,
  nodeId: string,
  iteration: number,
  attempt: number,
  lastHeartbeatAtMs: number,
  timeoutMs: number,
  timestampMs: number,
}
```

`lastHeartbeatAtMs`: Unix ms of the last heartbeat received before timeout. `timeoutMs`: The configured timeout duration.

### Sandbox Events

#### SandboxCreated

Emitted when a sandboxed execution environment is provisioned.

```ts
{
  type: "SandboxCreated",
  runId: string,
  sandboxId: string,
  runtime: "bubblewrap" | "docker" | "codeplane",
  configJson: string,
  timestampMs: number,
}
```

`sandboxId`: Unique identifier for this sandbox instance. `runtime`: The isolation backend used. `configJson`: JSON-serialized sandbox configuration.

#### SandboxShipped

Emitted when the initial code bundle has been uploaded to the sandbox.

```ts
{
  type: "SandboxShipped",
  runId: string,
  sandboxId: string,
  runtime: "bubblewrap" | "docker" | "codeplane",
  bundleSizeBytes: number,
  timestampMs: number,
}
```

`bundleSizeBytes`: Size of the uploaded bundle in bytes.

#### SandboxHeartbeat

Emitted periodically while a sandbox is executing to indicate liveness.

```ts
{
  type: "SandboxHeartbeat",
  runId: string,
  sandboxId: string,
  remoteRunId?: string,
  progress?: number,
  timestampMs: number,
}
```

`remoteRunId`: Run ID assigned by the remote sandbox environment, if available. `progress`: Optional 0–1 progress fraction reported by the sandbox.

#### SandboxBundleReceived

Emitted when the sandbox returns an output bundle to the orchestrator.

```ts
{
  type: "SandboxBundleReceived",
  runId: string,
  sandboxId: string,
  bundleSizeBytes: number,
  patchCount: number,
  hasOutputs: boolean,
  timestampMs: number,
}
```

`bundleSizeBytes`: Size of the received bundle. `patchCount`: Number of file patches included in the bundle. `hasOutputs`: Whether structured task outputs were included.

#### SandboxCompleted

Emitted when a sandbox execution finishes (regardless of outcome).

```ts
{
  type: "SandboxCompleted",
  runId: string,
  sandboxId: string,
  remoteRunId?: string,
  runtime: "bubblewrap" | "docker" | "codeplane",
  status: "finished" | "failed" | "cancelled",
  durationMs: number,
  timestampMs: number,
}
```

`status`: Final execution status. `durationMs`: Total sandbox execution time.

#### SandboxFailed

Emitted when a sandbox encounters an unrecoverable error.

```ts
{
  type: "SandboxFailed",
  runId: string,
  sandboxId: string,
  runtime: "bubblewrap" | "docker" | "codeplane",
  error: unknown,
  timestampMs: number,
}
```

#### SandboxDiffReviewRequested

Emitted when a sandbox produces patches that require human review before being applied.

```ts
{
  type: "SandboxDiffReviewRequested",
  runId: string,
  sandboxId: string,
  patchCount: number,
  totalDiffLines: number,
  timestampMs: number,
}
```

`patchCount`: Number of patches awaiting review. `totalDiffLines`: Total lines across all diffs.

#### SandboxDiffAccepted

Emitted when a human reviewer accepts the sandbox's proposed patches.

```ts
{
  type: "SandboxDiffAccepted",
  runId: string,
  sandboxId: string,
  patchCount: number,
  timestampMs: number,
}
```

#### SandboxDiffRejected

Emitted when a human reviewer rejects the sandbox's proposed patches.

```ts
{
  type: "SandboxDiffRejected",
  runId: string,
  sandboxId: string,
  reason?: string,
  timestampMs: number,
}
```

`reason`: Optional explanation for the rejection.

### Revert Events

#### RevertStarted

```ts
{
  type: "RevertStarted",
  runId: string,
  nodeId: string,
  iteration: number,
  attempt: number,
  jjPointer: string,
  timestampMs: number,
}
```

#### RevertFinished

```ts
{
  type: "RevertFinished",
  runId: string,
  nodeId: string,
  iteration: number,
  attempt: number,
  jjPointer: string,
  success: boolean,
  error?: string,
  timestampMs: number,
}
```

### Retry / Time-Travel Events

#### RetryTaskStarted

Emitted when a manual or programmatic retry is initiated for a specific task node.

```ts
{
  type: "RetryTaskStarted",
  runId: string,
  nodeId: string,
  iteration: number,
  resetDependents: boolean,
  resetNodes: string[],
  timestampMs: number,
}
```

`resetDependents`: Whether nodes that depend on this task are also being reset. `resetNodes`: Full list of node IDs being cleared as part of this retry.

#### RetryTaskFinished

Emitted when the retry operation completes.

```ts
{
  type: "RetryTaskFinished",
  runId: string,
  nodeId: string,
  iteration: number,
  resetNodes: string[],
  success: boolean,
  error?: string,
  timestampMs: number,
}
```

`resetNodes`: Node IDs that were actually reset. `error`: Set if the retry operation itself failed (not the retried task).

#### TimeTravelStarted

Emitted when a time-travel operation begins, rewinding the run to a prior state.

```ts
{
  type: "TimeTravelStarted",
  runId: string,
  nodeId: string,
  iteration: number,
  attempt: number,
  jjPointer?: string,
  timestampMs: number,
}
```

`jjPointer`: VCS change identifier to restore to, if VCS state is being rewound.

#### TimeTravelFinished

Emitted when the time-travel operation completes.

```ts
{
  type: "TimeTravelFinished",
  runId: string,
  nodeId: string,
  iteration: number,
  attempt: number,
  jjPointer?: string,
  success: boolean,
  vcsRestored: boolean,
  resetNodes: string[],
  error?: string,
  timestampMs: number,
}
```

`vcsRestored`: Whether VCS state was successfully rewound. `resetNodes`: Node IDs that were cleared as part of the rewind. `error`: Set if time-travel failed.

### Voice Events

#### VoiceStarted

Emitted when a voice operation begins.

```ts
{
  type: "VoiceStarted",
  runId: string,
  nodeId: string,
  iteration: number,
  operation: "speak" | "listen",
  provider: string,
  timestampMs: number,
}
```

`operation`: `"speak"` for text-to-speech; `"listen"` for speech-to-text. `provider`: The voice provider in use (e.g. `"openai"`, `"elevenlabs"`).

#### VoiceFinished

Emitted when a voice operation completes successfully.

```ts
{
  type: "VoiceFinished",
  runId: string,
  nodeId: string,
  iteration: number,
  operation: "speak" | "listen",
  provider: string,
  durationMs: number,
  timestampMs: number,
}
```

`durationMs`: Wall time for the voice operation.

#### VoiceError

Emitted when a voice operation fails.

```ts
{
  type: "VoiceError",
  runId: string,
  nodeId: string,
  iteration: number,
  operation: "speak" | "listen",
  provider: string,
  error: unknown,
  timestampMs: number,
}
```

### RAG Events

#### RagIngested

Emitted after documents are chunked and embedded into a vector store namespace.

```ts
{
  type: "RagIngested",
  runId: string,
  documentCount: number,
  chunkCount: number,
  namespace: string,
  timestampMs: number,
}
```

`documentCount`: Number of source documents ingested. `chunkCount`: Number of chunks stored after splitting. `namespace`: The vector store namespace written to.

#### RagRetrieved

Emitted after a semantic search query completes.

```ts
{
  type: "RagRetrieved",
  runId: string,
  query: string,
  resultCount: number,
  namespace: string,
  topScore: number,
  timestampMs: number,
}
```

`query`: The query string submitted. `resultCount`: Number of chunks returned. `topScore`: Similarity score of the highest-ranked result.

### Memory Events

#### MemoryFactSet

Emitted when a key-value fact is written to the memory store.

```ts
{
  type: "MemoryFactSet",
  runId: string,
  namespace: string,
  key: string,
  timestampMs: number,
}
```

`namespace`: Memory namespace the fact belongs to. `key`: Key under which the fact was stored.

#### MemoryRecalled

Emitted when the memory store is queried for relevant facts.

```ts
{
  type: "MemoryRecalled",
  runId: string,
  namespace: string,
  query: string,
  resultCount: number,
  timestampMs: number,
}
```

`query`: The recall query. `resultCount`: Number of facts returned.

#### MemoryMessageSaved

Emitted when a conversation message is persisted to memory.

```ts
{
  type: "MemoryMessageSaved",
  runId: string,
  threadId: string,
  role: string,
  timestampMs: number,
}
```

`threadId`: Identifier of the conversation thread. `role`: Message role (e.g. `"user"`, `"assistant"`).

### OpenAPI Events

#### OpenApiToolCalled

Emitted when a generated OpenAPI tool executes an HTTP operation.

```ts
{
  type: "OpenApiToolCalled",
  runId: string,
  operationId: string,
  method: string,
  path: string,
  durationMs: number,
  status: "success" | "error",
  timestampMs: number,
}
```

`operationId`: The OpenAPI `operationId` of the called operation. `method`: HTTP method (e.g. `"GET"`, `"POST"`). `path`: URL path template. `durationMs`: Round-trip duration. `status`: Whether the HTTP call succeeded or errored.

### Hot Reload

#### WorkflowReloadDetected

```ts
{
  type: "WorkflowReloadDetected",
  runId: string,
  changedFiles: string[],
  timestampMs: number
}
```

#### WorkflowReloaded

```ts
{
  type: "WorkflowReloaded",
  runId: string,
  generation: number,
  changedFiles: string[],
  timestampMs: number
}
```

`generation`: monotonically increasing reload counter.

#### WorkflowReloadFailed

```ts
{
  type: "WorkflowReloadFailed",
  runId: string,
  error: unknown,
  changedFiles: string[],
  timestampMs: number
}
```

The engine continues with the previous valid code.

#### WorkflowReloadUnsafe

```ts
{
  type: "WorkflowReloadUnsafe",
  runId: string,
  reason: string,
  changedFiles: string[],
  timestampMs: number
}
```

Schema changes require a process restart.

### Scorer Events

#### ScorerStarted

Emitted when a scorer begins evaluating a task's output.

```ts
{
  type: "ScorerStarted",
  runId: string,
  nodeId: string,
  scorerId: string,
  scorerName: string,
  timestampMs: number,
}
```

`scorerId`: Unique identifier of the scorer. `scorerName`: Human-readable scorer name.

#### ScorerFinished

Emitted when a scorer completes successfully.

```ts
{
  type: "ScorerFinished",
  runId: string,
  nodeId: string,
  scorerId: string,
  scorerName: string,
  score: number,
  timestampMs: number,
}
```

`score`: The 0–1 normalized score produced by the scorer.

#### ScorerFailed

Emitted when a scorer throws an error during evaluation.

```ts
{
  type: "ScorerFailed",
  runId: string,
  nodeId: string,
  scorerId: string,
  scorerName: string,
  error: unknown,
  timestampMs: number,
}
```

`error`: The error thrown by the scorer. Scorer failures never fail the parent task — they are logged and the workflow continues.

See [Evals & Scorers](/concepts/evals) for the full scoring system documentation.

## Quick Reference

| Event Type | Section | Extra Fields |
|---|---|---|
| `SupervisorStarted` | Supervisor | `pollIntervalMs`, `staleThresholdMs` |
| `SupervisorPollCompleted` | Supervisor | `staleCount`, `resumedCount`, `skippedCount`, `durationMs` |
| `RunStarted` | Run Lifecycle | -- |
| `RunStatusChanged` | Run Lifecycle | `status` |
| `RunFinished` | Run Lifecycle | -- |
| `RunFailed` | Run Lifecycle | `error` |
| `RunCancelled` | Run Lifecycle | -- |
| `RunAutoResumed` | Run Lifecycle | `lastHeartbeatAtMs`, `staleDurationMs` |
| `RunAutoResumeSkipped` | Run Lifecycle | `reason` |
| `RunContinuedAsNew` | Run Lifecycle | `newRunId`, `iteration`, `carriedStateSize`, `ancestryDepth?` |
| `RunForked` | Run Lifecycle | `parentRunId`, `parentFrameNo`, `branchLabel?` |
| `ReplayStarted` | Run Lifecycle | `parentRunId`, `parentFrameNo`, `restoreVcs` |
| `FrameCommitted` | Frame Events | `frameNo`, `xmlHash` |
| `SnapshotCaptured` | Snapshot | `frameNo`, `contentHash` |
| `NodePending` | Node Lifecycle | `nodeId`, `iteration` |
| `NodeStarted` | Node Lifecycle | `nodeId`, `iteration`, `attempt` |
| `NodeFinished` | Node Lifecycle | `nodeId`, `iteration`, `attempt` |
| `NodeFailed` | Node Lifecycle | `nodeId`, `iteration`, `attempt`, `error` |
| `NodeCancelled` | Node Lifecycle | `nodeId`, `iteration`, `attempt?`, `reason?` |
| `NodeSkipped` | Node Lifecycle | `nodeId`, `iteration` |
| `NodeRetrying` | Node Lifecycle | `nodeId`, `iteration`, `attempt` |
| `NodeWaitingApproval` | Node Lifecycle | `nodeId`, `iteration` |
| `NodeWaitingTimer` | Node Lifecycle | `nodeId`, `iteration`, `firesAtMs` |
| `ApprovalRequested` | Approval | `nodeId`, `iteration` |
| `ApprovalGranted` | Approval | `nodeId`, `iteration` |
| `ApprovalAutoApproved` | Approval | `nodeId`, `iteration` |
| `ApprovalDenied` | Approval | `nodeId`, `iteration` |
| `ToolCallStarted` | Tool | `nodeId`, `iteration`, `attempt`, `toolName`, `seq` |
| `ToolCallFinished` | Tool | `nodeId`, `iteration`, `attempt`, `toolName`, `seq`, `status` |
| `NodeOutput` | Output | `nodeId`, `iteration`, `attempt`, `text`, `stream` |
| `TimerCreated` | Timer | `timerId`, `firesAtMs`, `timerType` |
| `TimerFired` | Timer | `timerId`, `firesAtMs`, `firedAtMs`, `delayMs` |
| `TimerCancelled` | Timer | `timerId` |
| `TaskHeartbeat` | Task Heartbeat | `nodeId`, `iteration`, `attempt`, `hasData`, `dataSizeBytes`, `intervalMs?` |
| `TaskHeartbeatTimeout` | Task Heartbeat | `nodeId`, `iteration`, `attempt`, `lastHeartbeatAtMs`, `timeoutMs` |
| `SandboxCreated` | Sandbox | `sandboxId`, `runtime`, `configJson` |
| `SandboxShipped` | Sandbox | `sandboxId`, `runtime`, `bundleSizeBytes` |
| `SandboxHeartbeat` | Sandbox | `sandboxId`, `remoteRunId?`, `progress?` |
| `SandboxBundleReceived` | Sandbox | `sandboxId`, `bundleSizeBytes`, `patchCount`, `hasOutputs` |
| `SandboxCompleted` | Sandbox | `sandboxId`, `remoteRunId?`, `runtime`, `status`, `durationMs` |
| `SandboxFailed` | Sandbox | `sandboxId`, `runtime`, `error` |
| `SandboxDiffReviewRequested` | Sandbox | `sandboxId`, `patchCount`, `totalDiffLines` |
| `SandboxDiffAccepted` | Sandbox | `sandboxId`, `patchCount` |
| `SandboxDiffRejected` | Sandbox | `sandboxId`, `reason?` |
| `RevertStarted` | Revert | `nodeId`, `iteration`, `attempt`, `jjPointer` |
| `RevertFinished` | Revert | `nodeId`, `iteration`, `attempt`, `jjPointer`, `success`, `error?` |
| `RetryTaskStarted` | Retry / Time-Travel | `nodeId`, `iteration`, `resetDependents`, `resetNodes` |
| `RetryTaskFinished` | Retry / Time-Travel | `nodeId`, `iteration`, `resetNodes`, `success`, `error?` |
| `TimeTravelStarted` | Retry / Time-Travel | `nodeId`, `iteration`, `attempt`, `jjPointer?` |
| `TimeTravelFinished` | Retry / Time-Travel | `nodeId`, `iteration`, `attempt`, `jjPointer?`, `success`, `vcsRestored`, `resetNodes`, `error?` |
| `VoiceStarted` | Voice | `nodeId`, `iteration`, `operation`, `provider` |
| `VoiceFinished` | Voice | `nodeId`, `iteration`, `operation`, `provider`, `durationMs` |
| `VoiceError` | Voice | `nodeId`, `iteration`, `operation`, `provider`, `error` |
| `RagIngested` | RAG | `documentCount`, `chunkCount`, `namespace` |
| `RagRetrieved` | RAG | `query`, `resultCount`, `namespace`, `topScore` |
| `MemoryFactSet` | Memory | `namespace`, `key` |
| `MemoryRecalled` | Memory | `namespace`, `query`, `resultCount` |
| `MemoryMessageSaved` | Memory | `threadId`, `role` |
| `OpenApiToolCalled` | OpenAPI | `operationId`, `method`, `path`, `durationMs`, `status` |
| `AgentEvent` | Output | `nodeId`, `iteration`, `attempt`, `engine`, `event` |
| `WorkflowReloadDetected` | Hot Reload | `changedFiles` |
| `WorkflowReloaded` | Hot Reload | `generation`, `changedFiles` |
| `WorkflowReloadFailed` | Hot Reload | `error`, `changedFiles` |
| `WorkflowReloadUnsafe` | Hot Reload | `reason`, `changedFiles` |
| `RunHijackRequested` | Run Lifecycle | `target?` |
| `RunHijacked` | Run Lifecycle | `nodeId`, `iteration`, `attempt`, `engine`, `mode`, `resume?`, `cwd` |
| `ScorerStarted` | Scorer | `nodeId`, `scorerId`, `scorerName` |
| `ScorerFinished` | Scorer | `nodeId`, `scorerId`, `scorerName`, `score` |
| `ScorerFailed` | Scorer | `nodeId`, `scorerId`, `scorerName`, `error` |
| `TokenUsageReported` | Output | `nodeId`, `iteration`, `attempt`, `model`, `agent`, `inputTokens`, `outputTokens`, `cacheReadTokens?`, `cacheWriteTokens?`, `reasoningTokens?` |

## Persistence

Events are persisted in two places:

1. **SQLite** -- `_smithers_events` table with sequential `seq` number. Source of truth.
2. **NDJSON** -- `stream.ndjson` in the run's log directory. Best-effort.

Both are asynchronous. `onProgress` fires synchronously before persistence.

## Related

- [runWorkflow](/runtime/run-workflow) -- Where `onProgress` is configured.
- [Monitoring and Logs](/guides/monitoring-logs) -- Practical monitoring guide.
- [CLI](/cli/overview) -- View run status and frames.
