- Workflow definitions stay declarative. You write what the work is, not how to schedule it.
- Execution state is durable. Crash, restart, resume — the database is the truth.
- Branching and loops are data-driven. No hidden scheduler magic, no opaque state machines.
The Core Loop
Every run follows the same five-phase cycle:runWorkflow(...) drives that loop until the run finishes, fails, is cancelled, or pauses for approval.
If you’ve ever watched a React app re-render after a state change, you already have the intuition. The difference is that Smithers’ “state” lives in a database, and its “side effects” are agent calls and compute functions instead of DOM updates.
Phase 1: Render a Frame
Think of the workflow builder as a factory floor blueprint. Every time you render, you’re asking: given what we know right now, what does the floor plan look like? The workflow builder returned bycreateSmithers(...) is a pure function of the current context:
analyze hasn’t run yet, so outputMaybe returns nothing, and the review task doesn’t appear in the frame at all. After analyze finishes and its output is persisted, the next render sees that output and mounts review.
At render time Smithers does not execute tasks. It only builds a frame that reflects:
- the current
ctx.input - previously persisted outputs
- current loop iteration state
- mounted control-flow nodes such as
<Branch>,<Loop>, and<Approval>
Phase 2: Extract Task Descriptors
You have a rendered tree. Now what? Smithers walks that tree and extractsTaskDescriptor records — flat, serializable descriptions of each piece of work. Think of it as reading the blueprint and writing work orders.
Each descriptor includes things like:
nodeId- output destination
- task kind: agent, compute, or static
- retry and timeout policy
- dependency metadata
- loop iteration
- concurrency-group metadata from
<Parallel>or<MergeQueue> - worktree metadata from
<Worktree>
<Sequence>contributes ordering<Parallel>contributes a concurrency group<Branch>mounts exactly one branch for the current frame<Loop>remounts its children for the active iterationskipIfremoves nodes entirely from the current frame
<Branch> only mounts one branch, what happened to the other branches?” They simply aren’t in the frame. They don’t exist as far as the scheduler is concerned. If the data changes and a different branch should be active, the next render will mount it.
Phase 3: Schedule Ready Tasks
Now the scheduler looks at the descriptors and asks a simple question for each one: can this run right now? Typical task states are:| State | Meaning |
|---|---|
pending | Known to the workflow but not yet runnable |
running | Currently executing |
finished | Persisted successfully |
failed | Terminal failure after retries are exhausted |
waiting-approval | Blocked on a durable approval decision |
cancelled | Interrupted by an abort signal or explicit handoff |
skipped | Not mounted or intentionally bypassed |
- all sequential/dependency constraints are satisfied
- any required approval has been granted
- the current branch or loop iteration has mounted it
- its concurrency group allows another slot
Phase 4: Execute Tasks
Smithers supports three task execution modes. The simplest first:Static tasks
When the children are plain data and noagent is present, Smithers writes that payload directly. No computation, no network call. This is useful for seeding known values into the workflow.
Compute tasks
When the children are a function and noagent is present, Smithers runs that callback at execution time and persists the returned value. Your function, your logic, deterministic output.
Agent tasks
Whenagent is present, Smithers renders the children to markdown, sends that prompt to the agent, validates the returned JSON, and persists the validated row. This is where LLMs enter the picture.
Across all three modes, the runtime applies the same operational policies:
- timeout handling
- retries
continueOnFail- caching
- approval waits
- event emission
Phase 5: Persist Durable State
Here’s the aha moment. After each task attempt, Smithers writes durable records to SQLite. Not “optionally.” Not “if you configure a backend.” Every time, unconditionally. That includes:- the validated task output row
- attempt metadata
- node state transitions
- render frames
- lifecycle events
- approval decisions
- cache entries when enabled
(runId, nodeId, iteration) and render again.” On the next render, ctx.outputMaybe(...) and ctx.latest(...) read from persisted rows, not from in-memory task objects.
The database isn’t a log you consult after the fact. It’s the control plane.
Re-rendering Is the Control Plane
This is worth saying twice, because it’s the single idea that makes the rest of the system simple. Smithers does not mutate a long-lived in-memory graph after each task. Instead, it re-renders with the latest persisted context. The new frame might look different from the old one — a branch might activate, a loop might advance, a conditional task might appear for the first time. This is what enables:- dynamic branching based on completed outputs
- iterative loops that stop when a reviewer approves
- conditional task visibility
- hot reload for future work without restarting the run
Resume Semantics
When you run withresume: true, Smithers:
- loads the prior run metadata
- reloads persisted outputs and internal state
- renders the workflow again with that state available in
ctx - skips tasks that already have valid output rows
- continues from the first unfinished mounted work
id="review" to id="review-step" creates a new durable node from the runtime’s perspective. The old review output is still in the database, but nothing references it anymore. Be as creative as you like with control flow; be boring with your IDs.
Determinism
Where does determinism come from? Not from restricting what you can do. From making the inputs explicit:- stable
idprops identify tasks durably - JSX structure defines ordering and control flow
- persisted rows define what has already happened
- schema validation prevents malformed outputs from entering durable state
ctx, the system can reproduce it.
Mental Model
When in doubt, come back to this:- JSX decides what the current workflow frame looks like
- SQLite decides what has already happened
- the scheduler only runs work that is both mounted and unblocked
Next Steps
- Data Model — See how input, output tables, and internal metadata are stored.
- runWorkflow — Programmatic entry point for executing a workflow.
- Events — Lifecycle events emitted while the loop runs.
- Planner Internals — Lower-level details on extraction and scheduling.