for loop has no plan; it has a call stack, and a call stack dies with the process.
That is the failure mode declarative workflows exist to prevent. If your orchestration is a script, a crash at hour three means re-running from line one — or littering the script with hand-rolled checkpointing. If a human approval is a readline prompt, the process must stay alive until tomorrow morning. If your reviewer asks “what will this workflow do to prod,” the honest answer is “run it and see.”
Smithers’ answer: a workflow is a React function component. Calling it does not execute anything — it renders a tree that describes the tasks, their order, and their dependencies. The runtime extracts that tree into a graph, executes what’s ready, persists outputs, and renders again. The description is the artifact; execution is just the runtime walking it.
How the industry describes work
Every serious orchestrator has picked a position on the description-vs-code spectrum, and each position has a known cost.
The pattern: description-shaped systems (Step Functions, Argo, n8n) get inspection, diffing, and resumability for free but fight their format the moment control flow gets dynamic. Code-shaped systems (Temporal, Prefect) get natural control flow but must reconstruct the plan indirectly — Temporal by deterministic replay of an event history, with all the constraints that implies.
React’s reconciliation model is the escape hatch between these poles, and it predates workflow engines using it: a render function is code that produces a description (the element tree), and the reconciler diffs consecutive descriptions to compute minimal changes. That is exactly the shape a dynamic DAG needs — full programming language for control flow, static artifact per frame for the runtime. Smithers is a React host the way React Native and React Three Fiber are: same reconciler, different host elements. Why React? covers the agent-experience argument; this page covers the mechanics.
How Smithers does it
A workflow file default-exports the result ofsmithers((ctx) => <tree>). The tree is built from host components — <Workflow>, <Task>, <Sequence>, <Parallel>, <Branch>, <Loop> — that render to smithers:* host elements, never the DOM (jsxImportSource: "smithers-orchestrator" in tsconfig routes JSX to the workflow reconciler).
createSmithers(schemas)registers Zod schemas as durable output relations and returns typed components plusoutputs, sooutputs.analsisis a compile error, not a runtime surprise.- Dependencies are conditionals.
ctx.outputMaybe(ref, { nodeId })returnsundefineduntil the upstream task completes, so the dependent subtree simply isn’t in the plan yet — the conditional is the edge. For the common one-upstream case,<Task deps={{ analyze: outputs.analysis }}>with a(deps) => ...children callback is the direct form. NoResultPathplumbing, noadd_conditional_edges. - The render loop. Each frame: render (pure), extract the tree into a
GraphSnapshotofTaskDescriptors (id, ordinal, dependencies, schema, agent, retries, timeouts), dispatch ready tasks, persist validated outputs to per-schema tables, re-render against the new state. See How It Works. - MDX prompts. A prompt can be a
.smithers/prompts/*.mdxfile taking{props.field}interpolations; the.tsxrenders it at runtime, so prompt edits need no TypeScript. See MDX Workflow Authoring. - The Effect API is the same runtime with a React-free surface:
G.step("id", { needs: { analyze }, output, run }), composed withG.sequence/G.parallel/G.match/G.scope, finalized byG.from(...).needsis the explicit-edge counterpart of JSX’s conditionals. See Effect API.
Dynamic fan-out
The classic declarative-graph complaint — “I don’t know how many branches until runtime” — dissolves because the description is re-produced every frame. Fan out over data with.map() and React keys, exactly like rendering a list:
plan completes, the <Parallel> renders empty. The frame after, N subtrees mount — where Airflow needed .expand() and Step Functions needs a Map state, this is a JavaScript array method. subtreeConcurrency caps whole child subtrees in flight; maxConcurrency caps leaf tasks (they measure different things once children are subtrees). <Loop> covers iterate-until-done, re-rendering its body each iteration until until holds.
The description is what everything else stands on
Every frame’sGraphSnapshot is persisted. That single fact is why the rest of Smithers exists:
- Retries re-dispatch a
TaskDescriptor, not “re-run the script.” - Resume replays render against persisted state; completed tasks are in the state, so they never re-execute.
- Approvals suspend a node in a stored graph; the process can exit and the run survives until
smithers approve. - Hot reload (
smithers up workflow.tsx --hot) re-renders edited source against the same persisted state on the next frame, diffs the plans, and continues — in-flight task state intact. - UIs and time travel draw and fork frames because a frame is data, not a stack trace.
smithers graph is the payoff of description-shaped workflows in one command: the full plan, before a single token is spent. Use it in review the way you’d read a Step Functions definition — except it’s the real artifact, not a diagram that drifted.
Guarantees and limits
- Renders must be pure.
Date.now()orfetchin the render body produces unstable plans. Side effects go in task bodies, which are durable and retried. - IDs are the identity. Renaming a task id orphans its persisted output and creates a new node. Keep ids stable and data-derived (
`${ticket.id}-implement`), especially under fan-out — same rule as Temporal’s workflow versioning, smaller blast radius. - Hot reload is not resume.
--hotapplies edits within a running frame; resuming a stopped run validates the original source hash and fails withRESUME_METADATA_MISMATCHif the file changed. Edited workflow → fresh run. smithers graphvalidates shape, not semantics. It catches structural errors cheaply, but adepskey that doesn’t match any node id still fails at runtime, not at graph time.- Frames aren’t free. Every frame writes a snapshot; unbounded loops grow the database.
<ContinueAsNew>hands off to a fresh run with carried state. - Where a visual builder genuinely fits — non-engineers wiring SaaS triggers — n8n or Zapier is the right tool; Smithers assumes the author (human or agent) writes code.
Operating it
See also
- Why React? — the agent-experience argument for JSX as the surface.
- How It Works — render → extract → execute → persist, in depth.
- JSX API and Effect API — the two authoring surfaces.
- Components reference — every element and prop.
<Parallel>,<Loop>— concurrency and iteration semantics.