Skip to main content
An orchestrator has to answer one question before it can do anything durable: what is the plan? Not “what code will run” — a description of what will run. Without that description, retries have no unit to retry, resume has no position to resume at, approvals have nothing to attach to, and a UI has nothing to draw. An imperative script that calls agents in a 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 of smithers((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).
Key mechanics, each verifiable in the reference:
  • createSmithers(schemas) registers Zod schemas as durable output relations and returns typed components plus outputs, so outputs.analsis is a compile error, not a runtime surprise.
  • Dependencies are conditionals. ctx.outputMaybe(ref, { nodeId }) returns undefined until 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. No ResultPath plumbing, no add_conditional_edges.
  • The render loop. Each frame: render (pure), extract the tree into a GraphSnapshot of TaskDescriptors (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/*.mdx file taking {props.field} interpolations; the .tsx renders 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 with G.sequence/G.parallel/G.match/G.scope, finalized by G.from(...). needs is 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:
Until 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’s GraphSnapshot 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.
An imperative Temporal workflow gets these too — but only by making the event history the durable artifact and constraining your code to replay deterministically against it. Smithers moves the constraint: your render must be pure (side effects belong in task bodies), and in exchange the plan is a first-class, printable value. Printable literally:
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() or fetch in 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. --hot applies edits within a running frame; resuming a stopped run validates the original source hash and fails with RESUME_METADATA_MISMATCH if the file changed. Edited workflow → fresh run.
  • smithers graph validates shape, not semantics. It catches structural errors cheaply, but a deps key 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