Skip to main content
This page describes internal architecture. You do not need it to write workflows, but it is useful when designing new primitives or debugging scheduler behavior.
You don’t need this page to write workflows. But if you want to understand why they work — why a step waits for its dependencies, why a crash doesn’t lose progress, why the scheduler always knows what to run next — this is the page. Think of Smithers as a small compiler. Your JSX workflow definition is the source language. The output is not machine code but a durable execution plan — a data structure the scheduler can walk, suspend, resume, and recover without ever re-reading your original definition. The pipeline has six stages:
JSX Workflow
  ->
Workflow Graph
  ->
Normalized Plan
  ->
Durable Execution State
  ->
Ready Step Set
  ->
Effect Execution + Commit
We will walk through each one. By the end, you will know exactly what happens between the moment you write <Workflow> and the moment a step runs.

Stage 1: Builder Capture

Start with the simplest interesting example:
<Workflow name="bugfix">
  <Sequence>
    <Task id="analyze" output={outputs.analysis}>
      {{ summary: String(ctx.input.title) }}
    </Task>
    <Approval
      id="approve"
      output={outputs.approval}
      request={{ title: "Approve fix?" }}
      onDeny="fail"
    />
    <Task id="fix" output={outputs.fix}>
      {{ patch: "fix applied" }}
    </Task>
  </Sequence>
</Workflow>
What does Smithers do with this? It does not execute it. It captures a typed graph — nodes with explicit kinds, stable ids, and typed handles. No side effects, no network calls, just structure. Conceptually, the graph looks like this:
type WorkflowNode =
  | { kind: "step"; id: string; ... }
  | { kind: "sequence"; children: WorkflowNode[] }
  | { kind: "parallel"; children: WorkflowNode[]; maxConcurrency?: number }
  | { kind: "approval"; id: string; ... }
  | { kind: "match"; id: string; ... }
  | { kind: "loop"; id: string; ... };
Why does this matter? Because the graph is fully explicit before execution begins. The scheduler never has to interpret your JSX at runtime. It works from data. If you have written a compiler, this is the parse tree. If you have not written a compiler, think of it as a blueprint: the house is not built yet, but every wall, door, and wire is accounted for.

Stage 2: Normalization

A parse tree is not enough. You need to lower it into something the scheduler can work with mechanically. That is what normalization does. Specifically, normalization:
  • resolves step handles to stable internal ids
  • flattens needs into dependency edges
  • assigns concurrency groups
  • attaches output model signatures
  • attaches retry, timeout, cache, and approval policies
  • derives branch and loop controller descriptors
After this pass, every executable node is schedulable without touching your builder code again. The original JSX has served its purpose. From here on, the scheduler operates on plain data. If the analogy helps: this is the intermediate representation. The frontend is done.

Compiled Step Descriptor

So what does a normalized step actually look like? Here is the shape:
type CompiledStep = {
  id: string;
  outputModel: string;
  needs: ReadonlyArray<{ name: string; stepId: string }>;
  retryPolicy?: unknown;
  timeout?: unknown;
  cachePolicy?: unknown;
  concurrencyGroup?: string;
  run: unknown;
};
Notice that run is a callback, but everything else is plain data. That distinction is not an accident. The planner and persistence layer must reason about the workflow even when a step is not currently running — they need to inspect dependencies, check policies, and compute the ready set. They cannot do that if the interesting information is locked inside closures. Data you can inspect. Callbacks you can only call.

Stage 3: Durable Execution State

Here is where things get interesting. You have a normalized plan — a static description of what could happen. Now Smithers pairs it with persisted execution state — a record of what has happened. That state includes:
  • execution record
  • node state
  • completed outputs
  • attempt history
  • approval state
  • loop state
  • branch decisions
  • cache hits or invalidations
Why keep all of this? Because the plan tells the scheduler what is possible. The state tells it what has already occurred. Together, they answer the only question the scheduler cares about: what should run next? This is also what makes execution durable. If the process crashes after step two of five, the persisted state remembers those two completions. When the scheduler restarts, it rebuilds the ready set from the plan and the state, and picks up exactly where it left off. No guessing.

Stage 4: Ready-Set Computation

Now the scheduler walks the normalized plan and computes the ready set — the steps that can run right now. A step is ready when all of the following hold:
  • every entry in needs is completed
  • any enclosing sequence has advanced to it
  • any approval or branch controller has resolved in its favor
  • any loop state allows the current iteration
  • concurrency limits allow admission
That is the entire scheduling algorithm. No priority queues, no heuristics, no special cases. A step is either ready or it is not, and the answer comes from the graph edges and the current state. Why is this simpler than you might expect? Because the hard work happened in normalization. All the implicit ordering from <Sequence>, all the conditional logic from <Match>, all the iteration bookkeeping from loops — it was all lowered into explicit dependency edges and controller descriptors. The scheduler just reads them.

Stage 5: Execution

Ready steps are executed by invoking their run callback with:
  • validated input
  • resolved dependency outputs
  • execution metadata
  • cancellation signal
That is all the planner provides. It does not know about LLM providers, HTTP clients, or database drivers. It knows how to supply inputs and interpret one of five outcomes: success, failure, retry, suspension, or cancellation. This boundary is deliberate. The planner is a scheduler, not an application framework. Keeping it ignorant of provider-specific behavior means the same execution engine works regardless of what a step actually does.

Stage 6: Commit and Transition

After a step produces a result, Smithers commits:
  • the output model row
  • attempt metadata
  • lifecycle event records
  • state transitions for downstream scheduling
This commit is atomic at the workflow level wherever possible. The goal is simple: after a crash, Smithers recovers from persisted state without guessing. Either a step’s result was committed or it was not. There is no in-between to reconcile. Once the commit lands, the scheduler recomputes the ready set, and the cycle continues until no steps remain.

Debugging

When something goes wrong — and something always goes wrong — the most useful artifacts are:
  • normalized plan snapshots
  • node state tables
  • dependency edges
  • ready-set explanations
  • step transition events
If those five things are visible, most workflow bugs reduce to: “this step expected that dependency to be complete, and it was not.” The fix usually becomes obvious once you can see the edges.

Next Steps