10-second quickstart
Write the monitor at.smithers/monitor/<workflowId>.tsx, named after the workflow it watches:
up, workflow run, and the overridable oneshot workflow.
What a monitor is
A monitor is an ordinary workflow. Nothing about it is special except where the file lives and how it is launched:- It starts with the watched run, as a sibling run executing in parallel.
- Its
parent_run_idis the watched run, sobunx smithers-orchestrator psandbunx smithers-orchestrator inspect RUN_IDshow the pairing, the Gateway can render it, andbunx smithers-orchestrator cancel RUN_IDcascades to the monitor. - It is torn down when the watched run finishes, so a monitor never outlives what it watches.
- Its run id is
<watchedRunId>-monitor, and it carries asmithersMonitorForannotation naming the watched run. - A monitor never gets a monitor of its own. Three independent guards enforce that: the file’s location under
monitor/, the--no-monitorflag it is launched with, and theSMITHERS_MONITOR_SUPPRESSenvironment variable set on its process.
Writing one
Compose the shipped<Monitor> component rather than hand-rolling a poll loop. A monitor file is a few lines:
nodeId, runId, and iteration are reserved output columns, which is why the implicated node is reported as targetNodeId.
How the heartbeat works
Each beat is three steps, and the first beat samples immediately rather than sleeping first:- A durable
<Timer>paces the loop. - One agent task samples the watched run and classifies it into exactly one condition.
- A
<DecisionTable>routes that condition to its handler, first match wins.
maxChecks beats.
The condition set
The classifier must pick exactly one, which is what makes routing deterministic:What a monitor may do on its own
The shipped defaults are deliberately timid, because a monitor that guesses wrong makes an incident worse. Onlystalled and wedged-node heal without a human, and only because both repairs are idempotent and reversible: resuming re-enters the same durable frame, and retrying a node adds an attempt without discarding prior state. Running either twice is harmless.
Everything else escalates through a durable human request, which shows up in bunx smithers-orchestrator human list and in the Gateway. Cancelling, rewinding, reverting, time-travelling, resolving an approval on a human’s behalf, and editing the repository are never done autonomously.
Widen that authority explicitly when you mean to:
runaway-loop swaps its handler from “escalate” to “cancel the run”, which is destructive by design and therefore never a default.
Replace any handler outright, or silence one with null:
Reading run state
A monitor reads the watched run through the Gateway client (smithers-orchestrator/gateway-client) or the public CLI (status, inspect, events, node, why, ps). It must never open the store directly: .smithers/*.db, .smithers/pg, and smithers.db are private engine state, and reading them races the very run being watched.
The prompt shipped with <Monitor> binds the agent to that rule, along with what healthy and unhealthy look like, what evidence to gather before acting, and when to escalate instead of guessing. Extend it with repo-specific doctrine rather than replacing it:
prompt only when you need to replace the doctrine entirely.
Testing a monitor
A monitor is a workflow, so it tests like one, withcoverWorkflow or renderWorkflow from smithers-orchestrator/testing. Mock the classifier’s verdict and assert which handler ran:
examples/monitor-workflow.jsx for a complete, runnable monitor.