> ## Documentation Index
> Fetch the complete documentation index at: https://smithers.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Declarative Workflows

> Workflows are React components that render to a durable task graph — inspectable before execution, diffable across frames, and re-rendered as state changes.

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.

| System                                                                                                          | Authoring surface                                  | Mechanism                                                                           | Cost                                                                                                                                                                                                                           |
| --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [AWS Step Functions](https://docs.aws.amazon.com/step-functions/latest/dg/concepts-amazon-states-language.html) | Amazon States Language (JSON)                      | Fully static state machine; the service interprets the document                     | Notoriously verbose; data flow via `JSONPath`/`ResultPath` plumbing; conditionals as `Choice` state trees                                                                                                                      |
| [Temporal](https://docs.temporal.io/workflows)                                                                  | Imperative code, replayed                          | Event-history replay reconstructs state; code *is* the plan                         | [Determinism constraints](https://docs.temporal.io/workflow-definition#deterministic-constraints): no bare `Date.now()`, `Math.random()`, threads, or un-versioned code changes; the plan is only visible by replaying history |
| [Argo Workflows](https://argo-workflows.readthedocs.io/en/latest/walk-through/dag/)                             | YAML DAG/steps templates                           | Kubernetes CRD; `dependencies:` lists between templates                             | YAML templating (`{{item}}`, `withItems`) for anything dynamic; logic in string interpolation                                                                                                                                  |
| [Airflow](https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/dags.html)                        | Python builds a static DAG                         | DAG file parsed on a schedule; tasks wired with `>>`                                | The DAG must be known at parse time; [dynamic task mapping](https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/dynamic-task-mapping.html) (`.expand()`) was bolted on in 2.3 to fix this           |
| [Prefect](https://docs.prefect.io/v3/develop/write-flows) / [Dagster](https://docs.dagster.io/guides/build/ops) | Decorated Python (`@flow`/`@task`, `@op`/`@asset`) | Runtime discovers the graph as calls happen (Prefect) or from definitions (Dagster) | Prefect's graph is partly emergent — you learn the shape by running it                                                                                                                                                         |
| [LangGraph](https://langchain-ai.github.io/langgraph/concepts/low_level/)                                       | Explicit graph API                                 | `StateGraph.add_node` / `add_edge` / `add_conditional_edges`, then `compile()`      | Hand-wired adjacency lists; a 30-node graph is 60+ registration calls with string node names                                                                                                                                   |
| [n8n](https://n8n.io) / Zapier                                                                                  | Visual builder                                     | Nodes and wires, serialized JSON underneath                                         | Great to inspect, painful to diff, review, or generate programmatically                                                                                                                                                        |

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](https://legacy.reactjs.org/docs/reconciliation.html) are: same reconciler, different host elements. [Why React?](/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).

```tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
/** @jsxImportSource smithers-orchestrator */
import { createSmithers } from "smithers-orchestrator";
import { z } from "zod";

const { Workflow, Task, Sequence, Parallel, smithers, outputs } = createSmithers({
  input: z.object({ repo: z.string() }),
  analysis: z.object({ summary: z.string(), risk: z.enum(["low", "medium", "high"]) }),
  review: z.object({ verdict: z.string() }),
  report: z.object({ markdown: z.string() }),
});

export default smithers((ctx) => {
  const analysis = ctx.outputMaybe(outputs.analysis, { nodeId: "analyze" });

  return (
    <Workflow name="repo-review">
      <Sequence>
        <Task id="analyze" output={outputs.analysis} retries={2} timeoutMs={120_000}>
          {`Analyze ${ctx.input.repo} and assess migration risk.`}
        </Task>
        {analysis?.risk === "high" ? (
          <Parallel maxConcurrency={2}>
            <Task id="security-review" output={outputs.review}>Review for security regressions.</Task>
            <Task id="perf-review" output={outputs.review}>Review for performance regressions.</Task>
          </Parallel>
        ) : null}
        <Task id="report" deps={{ analyze: outputs.analysis }} output={outputs.report}>
          {(deps) => `Write the report. Summary: ${deps.analyze.summary}`}
        </Task>
      </Sequence>
    </Workflow>
  );
}, { output: outputs.report });
```

Key mechanics, each verifiable in the [reference](/reference/components):

* **`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 `TaskDescriptor`s (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](/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](/guides/mdx-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](/effect/overview).

### 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 `key`s, exactly like rendering a list:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
const tickets = ctx.outputMaybe(outputs.tickets, { nodeId: "plan" });

<Parallel id="tickets" subtreeConcurrency={4}>
  {(tickets?.items ?? []).map((t) => (
    <Sequence key={t.id} label={t.title}>
      <Task id={`${t.id}-implement`} agent={coder} output={outputs.impl}>
        Implement {t.title}
      </Task>
    </Sequence>
  ))}
</Parallel>
```

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](/components/parallel) 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`](/cli/overview).
* **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:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
smithers graph workflow.tsx --input '{"repo":"acme/api"}'   # render the graph, execute nothing
smithers graph workflow.tsx --compact                        # structure only, no prompt bodies
```

`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

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
smithers graph <workflow> [--input <json>] [--compact]   # render without executing
smithers up workflow.tsx --hot                           # run with hot reload while authoring
smithers workflow list                                   # discover .smithers/workflows
smithers tree <run-id>                                   # DevTools snapshot of a live run's tree
smithers inspect <run-id>                                # node-level state of the extracted graph
```

## See also

* [Why React?](/why-react) — the agent-experience argument for JSX as the surface.
* [How It Works](/how-it-works) — render → extract → execute → persist, in depth.
* [JSX API](/jsx/overview) and [Effect API](/effect/overview) — the two authoring surfaces.
* [Components reference](/reference/components) — every element and prop.
* [`<Parallel>`](/components/parallel), [`<Loop>`](/components/loop) — concurrency and iteration semantics.
