Skip to main content
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.

Subscribing

onProgress Callback

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

EventMetric
RunStartedsmithers.runs.total
NodeStartedsmithers.nodes.started
NodeFinishedsmithers.nodes.finished
NodeFailedsmithers.nodes.failed
Approval eventsApproval counters
trackSmithersEvent from smithers-orchestrator/observability exposes this mapping for custom integrations.

Common Fields

Every SmithersEvent:
FieldTypeDescription
typestringEvent type discriminator.
runIdstringRun this event belongs to.
timestampMsnumberUnix timestamp in milliseconds.
Node-scoped events add:
FieldTypeDescription
nodeIdstringTask node ID.
iterationnumberLoop iteration number.
Attempt-scoped events add:
FieldTypeDescription
attemptnumberAttempt number (starts at 1).

Event Types

Run Lifecycle

RunStarted

Emitted once at the beginning of every run (including resumes).
{ type: "RunStarted", runId: string, timestampMs: number }

RunStatusChanged

{ type: "RunStatusChanged", runId: string, status: RunStatus, timestampMs: number }
RunStatus: "running" | "waiting-approval" | "finished" | "failed" | "cancelled".

RunFinished

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

RunFailed

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

RunCancelled

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

Frame Events

FrameCommitted

Emitted each time the engine renders a new frame.
{
  type: "FrameCommitted",
  runId: string,
  frameNo: number,
  xmlHash: string,
  timestampMs: number,
}
xmlHash: SHA-256 hex digest of the canonicalized XML tree.

Node Lifecycle

NodePending

Task identified, waiting to be scheduled.
{ type: "NodePending", runId: string, nodeId: string, iteration: number, timestampMs: number }

NodeStarted

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

NodeFinished

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

NodeFailed

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

NodeCancelled

{
  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

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

NodeRetrying

Fires before the next attempt starts.
{
  type: "NodeRetrying",
  runId: string,
  nodeId: string,
  iteration: number,
  attempt: number,
  timestampMs: number,
}
attempt is the upcoming attempt number.

NodeWaitingApproval

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

Approval Events

ApprovalRequested

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

ApprovalGranted

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

ApprovalDenied

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

Tool Events

ToolCallStarted

{
  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

{
  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.
{
  type: "NodeOutput",
  runId: string,
  nodeId: string,
  iteration: number,
  attempt: number,
  text: string,
  stream: "stdout" | "stderr",
  timestampMs: number,
}

Revert Events

RevertStarted

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

RevertFinished

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

Hot Reload

WorkflowReloadDetected

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

WorkflowReloaded

{
  type: "WorkflowReloaded",
  runId: string,
  generation: number,
  changedFiles: string[],
  timestampMs: number
}
generation: monotonically increasing reload counter.

WorkflowReloadFailed

{
  type: "WorkflowReloadFailed",
  runId: string,
  error: unknown,
  changedFiles: string[],
  timestampMs: number
}
The engine continues with the previous valid code.

WorkflowReloadUnsafe

{
  type: "WorkflowReloadUnsafe",
  runId: string,
  reason: string,
  changedFiles: string[],
  timestampMs: number
}
Schema changes require a process restart.

Quick Reference

Event TypeExtra Fields
RunStarted
RunStatusChangedstatus
RunFinished
RunFailederror
RunCancelled
FrameCommittedframeNo, xmlHash
NodePendingnodeId, iteration
NodeStartednodeId, iteration, attempt
NodeFinishednodeId, iteration, attempt
NodeFailednodeId, iteration, attempt, error
NodeCancellednodeId, iteration, attempt?, reason?
NodeSkippednodeId, iteration
NodeRetryingnodeId, iteration, attempt
NodeWaitingApprovalnodeId, iteration
ApprovalRequestednodeId, iteration
ApprovalGrantednodeId, iteration
ApprovalDeniednodeId, iteration
ToolCallStartednodeId, iteration, attempt, toolName, seq
ToolCallFinishednodeId, iteration, attempt, toolName, seq, status
NodeOutputnodeId, iteration, attempt, text, stream
AgentEventnodeId, iteration, attempt, engine, event
RevertStartednodeId, iteration, attempt, jjPointer
RevertFinishednodeId, iteration, attempt, jjPointer, success, error?
WorkflowReloadDetectedchangedFiles
WorkflowReloadedgeneration, changedFiles
WorkflowReloadFailederror, changedFiles
WorkflowReloadUnsafereason, changedFiles
RunHijackRequestedtarget?
RunHijackednodeId, iteration, attempt, engine, mode, resume?, cwd
ScorerStartednodeId, scorerId, scorerName
ScorerFinishednodeId, scorerId, scorerName, score
ScorerFailednodeId, scorerId, scorerName, error
TokenUsageReportednodeId, 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. NDJSONstream.ndjson in the run’s log directory. Best-effort.
Both are asynchronous. onProgress fires synchronously before persistence.