# Smithers Events

> Smithers event surface: how to subscribe, the event categories, and the full SmithersEvent discriminated union.

---

## Events

> Subscribe to lifecycle events. Full event union lives in Types.

`SmithersEvent` is the discriminated union of lifecycle events the runtime and observability layer understand. Most variants are emitted by the runtime; reserved variants are called out in [Event Types](/reference/event-types). The full type definition lives in [Types](/reference/types); that's the source of truth for field shapes.

## Subscribe via `onProgress`

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

await Effect.runPromise(runWorkflow(workflow, {
  input: { task: "fix bug" },
  onProgress: (event) => {
    if (event.type === "NodeStarted")  console.log(`▶ ${event.nodeId} (attempt ${event.attempt})`);
    if (event.type === "NodeFinished") console.log(`✓ ${event.nodeId}`);
    if (event.type === "NodeFailed")   console.error(`✗ ${event.nodeId}`, event.error);
  },
}));
```

## Read from the NDJSON log

Events append to `.smithers/executions/<runId>/logs/stream.ndjson` (configure with `logDir` / `--log-dir`; disable with `--no-log`).

```bash
tail -f .smithers/executions/<runId>/logs/stream.ndjson | jq .
jq 'select(.type == "NodeFailed")' .smithers/executions/<runId>/logs/stream.ndjson
jq -r .type .smithers/executions/<runId>/logs/stream.ndjson | sort | uniq -c | sort -rn
```

Or with the CLI:

```bash
bunx smithers-orchestrator events RUN_ID --json
bunx smithers-orchestrator events RUN_ID --type tool-call --node analyze
```

## Common fields

```ts
type CommonFields    = { type: string; runId: string; timestampMs: number };
type NodeScoped      = CommonFields & { nodeId: string; iteration: number };
type AttemptScoped   = NodeScoped   & { attempt: number };
```

Every event includes `type`, `runId`, `timestampMs`. Node-scoped events add `nodeId` and `iteration`. Attempt-scoped add `attempt`.

## Event categories

Used by `bunx smithers-orchestrator events --type <category>` and the metrics layer.

| Category | Events |
|---|---|
| `run` | RunAutoResumed, RunAutoResumeSkipped, RunStarted, RunStatusChanged, RunStateChanged, RunFinished, RunFailed, RunCancelled, RunContinuedAsNew, RunHijackRequested, RunHijacked, RetryTaskStarted, RetryTaskFinished, RunForked, ReplayStarted |
| `frame` | FrameCommitted |
| `node` | NodePending, NodeStarted, TaskHeartbeat, TaskHeartbeatTimeout, NodeFinished, NodeFailed, NodeCancelled, NodeSkipped, NodeRetrying, NodeWaitingApproval, NodeWaitingTimer |
| `approval` | ApprovalRequested, ApprovalGranted, ApprovalAutoApproved, ApprovalDenied |
| `tool-call` | ToolCallStarted, ToolCallFinished |
| `agent` | AgentEvent, AgentTraceEvent, AgentTraceSummary, AgentSessionEvent |
| `output` | NodeOutput |
| `revert` | RevertStarted, RevertFinished, TimeTravelStarted, TimeTravelFinished, TimeTravelJumped |
| `workflow` | WorkflowReloadDetected, WorkflowReloaded, WorkflowReloadFailed, WorkflowReloadUnsafe |
| `scorer` | ScorerStarted, ScorerFinished, ScorerFailed |
| `token` | TokenUsageReported |
| `timer` | TimerCreated, TimerFired, TimerCancelled |
| `memory` | MemoryFactSet, MemoryRecalled, MemoryMessageSaved |
| `openapi` | OpenApiToolCalled |
| `sandbox` | SandboxCreated, SandboxShipped, SandboxHeartbeat, SandboxBundleReceived, SandboxCompleted, SandboxFailed, SandboxDiffReviewRequested, SandboxDiffAccepted, SandboxDiffRejected |
| `snapshot` | SnapshotCaptured |
| `supervisor` | SupervisorStarted, SupervisorPollCompleted |

`RunStateChanged` is categorized as `run` because the type is part of the public event union, but the current runtime does not emit it. Use `RunStatusChanged` from the stream plus `getRun` / `computeRunState` for derived run state.

`OpenApiToolCalled` is categorized as `openapi` for forward compatibility, but the current OpenAPI tool factory records Effect metrics and log spans rather than emitting that event onto the run event bus.

## Built-in metrics

| Event | Metric |
|---|---|
| `RunStarted` | `smithers.runs.total` |
| `NodeStarted` | `smithers.nodes.started` |
| `NodeFinished` | `smithers.nodes.finished` |
| `NodeFailed` | `smithers.nodes.failed` |
| `ApprovalGranted` / `ApprovalDenied` | Approval counters |
| `TokenUsageReported` | Token usage counters per model/agent |

`trackSmithersEvent` from `smithers-orchestrator/observability` exposes this mapping for custom integrations. See [Observability](/guides/monitoring-logs) for the full OTLP/Prometheus setup.

---

## Event Types

> Current SmithersEvent variants, event categories, and top-level fields.

`SmithersEvent` is the discriminated union understood by the runtime and observability layer. Runtime-emitted variants are persisted to the event log, passed to `runWorkflow({ onProgress })`, streamed by Gateway, and filtered by `bunx smithers-orchestrator events --type <category>`. Reserved variants are documented here when they are typed but not currently emitted.

Every variant has `type`, `runId`, and `timestampMs`. Node-scoped variants add `nodeId` and `iteration`. Attempt-scoped variants add `attempt`.

For subscription examples, CLI usage, and built-in metrics, see [Events](/runtime/events).

## Status Types

```ts
type RunStatus =
  | "running"
  | "waiting-approval"
  | "waiting-event"
  | "waiting-timer"
  | "waiting-quota"
  | "paused"
  | "finished"
  | "continued"
  | "failed"
  | "cancelled";

type RunState =
  | "running"
  | "waiting-approval"
  | "waiting-event"
  | "waiting-timer"
  | "waiting-quota"
  | "paused"
  | "recovering"
  | "stale"
  | "orphaned"
  | "failed"
  | "cancelled"
  | "succeeded"
  | "unknown";
```

## Event Variants

Discriminate on the `type` field, e.g. `event.type === "NodeFailed"`, to handle specific lifecycle moments.

<Warning>
**In a browser UI the shape is different.** `useGatewayRunEvents` (and the vanilla
`streamRunEventsResilient`) yield **`GatewayEventFrame`** objects, not raw
`SmithersEvent`s. On a frame the event NAME is `frame.event` and every other field
lives under `frame.payload`. So the client-side equivalents are:

```tsx
const { events } = useGatewayRunEvents(runId, { afterSeq: 0 });
for (const frame of events) {
  if (frame.event === "NodeFailed") {
    const { nodeId, error } = frame.payload;   // NOT frame.type / frame.nodeId
    // error is the errorToJson value: read error.message
  }
}
```

Use `frame.event === "NodeStarted" | "NodeFinished" | "NodeFailed"` to build a
per-node status view, and `frame.payload.nodeId` to key it. The table below lists
each variant's fields; in a UI those fields are on `frame.payload`.
</Warning>

| Event | Top-level fields |
|---|---|
| `SupervisorStarted` | `runId`, `pollIntervalMs`, `staleThresholdMs`, `timestampMs` |
| `SupervisorPollCompleted` | `runId`, `staleCount`, `resumedCount`, `skippedCount`, `durationMs`, `timestampMs` |
| `RunAutoResumed` | `runId`, `lastHeartbeatAtMs`, `staleDurationMs`, `timestampMs` |
| `RunAutoResumeSkipped` | `runId`, `reason`, `timestampMs` |
| `RunStarted` | `runId`, `timestampMs` |
| `RunStatusChanged` | `runId`, `status`, `timestampMs` |
| `RunStateChanged` | `runId`, `before`, `after`, `timestampMs` |
| `RunFinished` | `runId`, `timestampMs` |
| `RunFailed` | `runId`, `error`, `timestampMs` |
| `RunCancelled` | `runId`, `timestampMs` |
| `RunContinuedAsNew` | `runId`, `newRunId`, `iteration`, `carriedStateSize`, `ancestryDepth?`, `timestampMs` |
| `RunHijackRequested` | `runId`, `target?`, `timestampMs` |
| `RunHijacked` | `runId`, `nodeId`, `iteration`, `attempt`, `engine`, `mode`, `resume?`, `cwd`, `timestampMs` |
| `SandboxCreated` | `runId`, `sandboxId`, `runtime`, `configJson`, `timestampMs` |
| `SandboxShipped` | `runId`, `sandboxId`, `runtime`, `bundleSizeBytes`, `timestampMs` |
| `SandboxHeartbeat` | `runId`, `sandboxId`, `remoteRunId?`, `progress?`, `timestampMs` |
| `SandboxBundleReceived` | `runId`, `sandboxId`, `bundleSizeBytes`, `patchCount`, `hasOutputs`, `timestampMs` |
| `SandboxCompleted` | `runId`, `sandboxId`, `remoteRunId?`, `runtime`, `status`, `durationMs`, `timestampMs` |
| `SandboxFailed` | `runId`, `sandboxId`, `runtime`, `error`, `timestampMs` |
| `SandboxDiffReviewRequested` | `runId`, `sandboxId`, `patchCount`, `totalDiffLines`, `timestampMs` |
| `SandboxDiffAccepted` | `runId`, `sandboxId`, `patchCount`, `timestampMs` |
| `SandboxDiffRejected` | `runId`, `sandboxId`, `reason?`, `timestampMs` |
| `FrameCommitted` | `runId`, `frameNo`, `xmlHash`, `timestampMs` |
| `NodePending` | `runId`, `nodeId`, `iteration`, `timestampMs` |
| `NodeStarted` | `runId`, `nodeId`, `iteration`, `attempt`, `timestampMs` |
| `TaskHeartbeat` | `runId`, `nodeId`, `iteration`, `attempt`, `hasData`, `dataSizeBytes`, `intervalMs?`, `timestampMs` |
| `TaskHeartbeatTimeout` | `runId`, `nodeId`, `iteration`, `attempt`, `lastHeartbeatAtMs`, `timeoutMs`, `timestampMs` |
| `NodeFinished` | `runId`, `nodeId`, `iteration`, `attempt`, `timestampMs` |
| `NodeFailed` | `runId`, `nodeId`, `iteration`, `attempt`, `error`, `timestampMs` |
| `NodeCancelled` | `runId`, `nodeId`, `iteration`, `attempt?`, `reason?`, `timestampMs` |
| `NodeSkipped` | `runId`, `nodeId`, `iteration`, `timestampMs` |
| `NodeRetrying` | `runId`, `nodeId`, `iteration`, `attempt`, `timestampMs` |
| `NodeWaitingApproval` | `runId`, `nodeId`, `iteration`, `timestampMs` |
| `NodeWaitingTimer` | `runId`, `nodeId`, `iteration`, `firesAtMs`, `timestampMs` |
| `ApprovalRequested` | `runId`, `nodeId`, `iteration`, `timestampMs` |
| `ApprovalGranted` | `runId`, `nodeId`, `iteration`, `timestampMs` |
| `ApprovalAutoApproved` | `runId`, `nodeId`, `iteration`, `timestampMs` |
| `ApprovalDenied` | `runId`, `nodeId`, `iteration`, `timestampMs` |
| `ToolCallStarted` | `runId`, `nodeId`, `iteration`, `attempt`, `toolCallId`, `toolName`, `seq`, `timestampMs` |
| `ToolCallFinished` | `runId`, `nodeId`, `iteration`, `attempt`, `toolCallId`, `toolName`, `seq`, `status`, `timestampMs` |
| `NodeOutput` | `runId`, `nodeId`, `iteration`, `attempt`, `text`, `stream`, `timestampMs` |
| `AgentEvent` | `runId`, `nodeId`, `iteration`, `attempt`, `engine`, `event`, `timestampMs` |
| `RetryTaskStarted` | `runId`, `nodeId`, `iteration`, `resetDependents`, `resetNodes`, `timestampMs` |
| `RetryTaskFinished` | `runId`, `nodeId`, `iteration`, `resetNodes`, `success`, `error?`, `timestampMs` |
| `RevertStarted` | `runId`, `nodeId`, `iteration`, `attempt`, `jjPointer`, `timestampMs` |
| `RevertFinished` | `runId`, `nodeId`, `iteration`, `attempt`, `jjPointer`, `success`, `error?`, `timestampMs` |
| `TimeTravelStarted` | `runId`, `nodeId`, `iteration`, `attempt`, `jjPointer?`, `timestampMs` |
| `TimeTravelFinished` | `runId`, `nodeId`, `iteration`, `attempt`, `jjPointer?`, `success`, `vcsRestored`, `resetNodes`, `error?`, `timestampMs` |
| `TimeTravelJumped` | `runId`, `fromFrameNo`, `toFrameNo`, `timestampMs`, `caller?` |
| `WorkflowReloadDetected` | `runId`, `changedFiles`, `timestampMs` |
| `WorkflowReloaded` | `runId`, `generation`, `changedFiles`, `timestampMs` |
| `WorkflowReloadFailed` | `runId`, `error`, `changedFiles`, `timestampMs` |
| `WorkflowReloadUnsafe` | `runId`, `reason`, `changedFiles`, `timestampMs` |
| `ScorerStarted` | `runId`, `nodeId`, `scorerId`, `scorerName`, `timestampMs` |
| `ScorerFinished` | `runId`, `nodeId`, `scorerId`, `scorerName`, `score`, `timestampMs` |
| `ScorerFailed` | `runId`, `nodeId`, `scorerId`, `scorerName`, `error`, `timestampMs` |
| `TokenUsageReported` | `runId`, `nodeId`, `iteration`, `attempt`, `model`, `agent`, `inputTokens`, `outputTokens`, `cacheReadTokens?`, `cacheWriteTokens?`, `reasoningTokens?`, `timestampMs` |
| `SnapshotCaptured` | `runId`, `frameNo`, `contentHash`, `timestampMs` |
| `RunForked` | `runId`, `parentRunId`, `parentFrameNo`, `branchLabel?`, `timestampMs` |
| `ReplayStarted` | `runId`, `parentRunId`, `parentFrameNo`, `restoreVcs`, `timestampMs` |
| `MemoryFactSet` | `runId`, `namespace`, `key`, `timestampMs` |
| `MemoryRecalled` | `runId`, `namespace`, `query`, `resultCount`, `timestampMs` |
| `MemoryMessageSaved` | `runId`, `threadId`, `role`, `timestampMs` |
| `OpenApiToolCalled` | `runId`, `operationId`, `method`, `path`, `durationMs`, `status`, `timestampMs` |
| `TimerCreated` | `runId`, `timerId`, `firesAtMs`, `timerType`, `timestampMs` |
| `TimerFired` | `runId`, `timerId`, `firesAtMs`, `firedAtMs`, `delayMs`, `timestampMs` |
| `TimerCancelled` | `runId`, `timerId`, `timestampMs` |
| `AgentTraceEvent` | `runId`, `nodeId`, `iteration`, `attempt`, `trace`, `timestampMs` |
| `AgentTraceSummary` | `runId`, `nodeId`, `iteration`, `attempt`, `summary`, `timestampMs` |
| `AgentSessionEvent` | `runId`, `nodeId`, `iteration`, `attempt`, `transcript`, `timestampMs` |

`RunStateChanged` is typed and categorized for forward compatibility, but the current runtime does not emit it. Read derived run state with `getRun` or `computeRunState`, and watch `RunStatusChanged` for persisted status transitions.

`OpenApiToolCalled` is typed and categorized for forward compatibility, but the current OpenAPI tool factory records Effect metrics and log spans rather than emitting that event onto the run event bus.

## Agent Event Payload

This type describes the `.event` field of the `AgentEvent` variant above. For structured traces, prefer `AgentTraceEvent`, `AgentTraceSummary`, and `AgentSessionEvent`.

`AgentEvent.event` is the normalized CLI-agent event payload:

```ts
type AgentCliEvent =
  | { type: "started"; engine: string; title: string; resume?: string; detail?: Record<string, unknown> }
  | {
      type: "action";
      engine: string;
      phase: "started" | "updated" | "completed";
      entryType?: "thought" | "message";
      action: { id: string; kind: AgentCliActionKind; title: string; detail?: Record<string, unknown> };
      message?: string;
      ok?: boolean;
      level?: "debug" | "info" | "warning" | "error";
    }
  | { type: "completed"; engine: string; ok: boolean; answer?: string; error?: string; resume?: string; usage?: Record<string, unknown> };
```
