Skip to main content
Smithers emits typed events for every lifecycle transition in a workflow run. These events are persisted to the SQLite database, written to NDJSON log files, and optionally streamed via Server-Sent Events (SSE). This guide covers all monitoring approaches.

onProgress Callback

The most direct way to monitor a run is the onProgress callback passed to runWorkflow:
import { runWorkflow } from "smithers-orchestrator";
import workflow from "./workflow";

const result = await runWorkflow(workflow, {
  input: { description: "Fix auth bugs" },
  onProgress: (event) => {
    switch (event.type) {
      case "RunStarted":
        console.log(`Run started: ${event.runId}`);
        break;
      case "NodeStarted":
        console.log(`Task started: ${event.nodeId} (attempt ${event.attempt})`);
        break;
      case "NodeFinished":
        console.log(`Task finished: ${event.nodeId}`);
        break;
      case "NodeFailed":
        console.log(`Task failed: ${event.nodeId} -- ${event.error}`);
        break;
      case "NodeRetrying":
        console.log(`Task retrying: ${event.nodeId} (attempt ${event.attempt})`);
        break;
      case "ApprovalRequested":
        console.log(`Approval needed: ${event.nodeId}`);
        break;
      case "RunFinished":
        console.log(`Run finished: ${event.runId}`);
        break;
      case "ToolCallStarted":
        console.log(`Tool call: ${event.toolName}`);
        break;
    }
  },
});
The callback is invoked synchronously with every event during the run. It receives a typed SmithersEvent object.

NDJSON Log Files

By default, Smithers writes every event as a newline-delimited JSON line to:
.smithers/executions/<runId>/logs/stream.ndjson
Each line is a self-contained JSON object:
{"type":"RunStarted","runId":"smth_a1b2c3d4","timestampMs":1730000000000}
{"type":"NodeStarted","runId":"smth_a1b2c3d4","nodeId":"analyze","iteration":0,"attempt":1,"timestampMs":1730000000100}
{"type":"ToolCallStarted","runId":"smth_a1b2c3d4","nodeId":"analyze","toolName":"read","timestampMs":1730000000200}
{"type":"ToolCallFinished","runId":"smth_a1b2c3d4","nodeId":"analyze","toolName":"read","durationMs":45,"timestampMs":1730000000245}
{"type":"NodeFinished","runId":"smth_a1b2c3d4","nodeId":"analyze","iteration":0,"attempt":1,"timestampMs":1730000005000}
{"type":"RunFinished","runId":"smth_a1b2c3d4","status":"finished","timestampMs":1730000010000}

Live tailing

Watch events in real time during execution:
tail -f .smithers/executions/<runId>/logs/stream.ndjson

Filtering with jq

# Pretty-print all events
cat .smithers/executions/<runId>/logs/stream.ndjson | jq .

# Show only node lifecycle events
cat .smithers/executions/<runId>/logs/stream.ndjson | jq 'select(.type | startswith("Node"))'

# Show failures with error details
cat .smithers/executions/<runId>/logs/stream.ndjson | jq 'select(.type == "NodeFailed")'

# Calculate task durations
cat .smithers/executions/<runId>/logs/stream.ndjson | jq 'select(.type == "NodeFinished") | {nodeId, durationMs: (.timestampMs - .timestampMs)}'

# Count events by type
cat .smithers/executions/<runId>/logs/stream.ndjson | jq -r '.type' | sort | uniq -c | sort -rn

Custom log directory

# Write logs to a custom directory
bunx smithers run workflow.tsx --log-dir ./my-logs

# Disable log output entirely
bunx smithers run workflow.tsx --no-log

SSE Endpoint (Server)

When using the Smithers server, you can stream events via Server-Sent Events:
GET /v1/runs/:runId/events
This endpoint sends events as they occur, formatted as SSE:
event: NodeStarted
data: {"type":"NodeStarted","runId":"smth_a1b2c3d4","nodeId":"analyze","iteration":0,"attempt":1,"timestampMs":1730000000100}

event: NodeFinished
data: {"type":"NodeFinished","runId":"smth_a1b2c3d4","nodeId":"analyze","iteration":0,"attempt":1,"timestampMs":1730000005000}
Connect from a browser or any SSE client:
const eventSource = new EventSource("http://localhost:3000/v1/runs/smth_a1b2c3d4/events");

eventSource.addEventListener("NodeStarted", (e) => {
  const event = JSON.parse(e.data);
  console.log(`Task started: ${event.nodeId}`);
});

eventSource.addEventListener("NodeFinished", (e) => {
  const event = JSON.parse(e.data);
  console.log(`Task finished: ${event.nodeId}`);
});

Event Types Reference

Smithers emits 25+ event types. Here is the full list grouped by category:

Run lifecycle

EventDescription
RunStartedA new run has been created
RunStatusChangedThe run status transitioned (e.g., running to waiting-approval)
RunFinishedThe run completed successfully
RunFailedThe run failed
RunCancelledThe run was cancelled (abort signal or manual)

Node lifecycle

EventDescription
NodePendingA node has been extracted from the tree and is waiting to be scheduled
NodeStartedA node’s execution has begun (includes nodeId, iteration, attempt)
NodeFinishedA node completed successfully
NodeFailedA node failed (includes error details)
NodeCancelledA node was cancelled (unmounted between renders or run aborted)
NodeSkippedA node was skipped (skipIf or Branch not taken)
NodeRetryingA node failed and will be retried (includes attempt number)
NodeOutputA node produced output (includes the output payload)

Approval events

EventDescription
NodeWaitingApprovalA node with needsApproval is waiting for a decision
ApprovalRequestedAn approval request has been created in the database
ApprovalGrantedA node was approved
ApprovalDeniedA node was denied

Tool events

EventDescription
ToolCallStartedAn agent tool invocation has started (includes tool name and arguments)
ToolCallFinishedAn agent tool invocation completed (includes result and duration)

Frame events

EventDescription
FrameCommittedA new render frame was persisted to the database

Revert events

EventDescription
RevertStartedA revert operation has begun
RevertFinishedA revert operation completed

Database Event Storage

All events are persisted to the _smithers_events table with sequential ordering:
# View the last 20 events for a run
sqlite3 smithers.db "SELECT seq, type, payload_json FROM _smithers_events WHERE run_id = '<id>' ORDER BY seq DESC LIMIT 20;"

# Count events by type
sqlite3 smithers.db "SELECT type, COUNT(*) FROM _smithers_events WHERE run_id = '<id>' GROUP BY type ORDER BY COUNT(*) DESC;"

# Find the timestamp of the last event
sqlite3 smithers.db "SELECT type, json_extract(payload_json, '$.timestampMs') as ts FROM _smithers_events WHERE run_id = '<id>' ORDER BY seq DESC LIMIT 1;"
The _smithers_events table has these columns:
ColumnTypeDescription
run_idtextThe run ID
seqintegerAuto-incrementing sequence number (unique per run)
typetextThe event type name
payload_jsontextFull event payload as JSON

Building a Progress Dashboard

Combine the onProgress callback with your own state tracking to build a real-time dashboard:
const nodeStatuses = new Map<string, string>();

await runWorkflow(workflow, {
  input: { description: "Fix bugs" },
  onProgress: (event) => {
    if ("nodeId" in event) {
      nodeStatuses.set(event.nodeId, event.type);
    }

    // Clear the terminal and redraw
    console.clear();
    for (const [nodeId, status] of nodeStatuses) {
      const icon = status === "NodeFinished" ? "[done]"
        : status === "NodeStarted" ? "[....]"
        : status === "NodeFailed" ? "[FAIL]"
        : "[    ]";
      console.log(`${icon} ${nodeId}`);
    }
  },
});

Next Steps