Effect, Layer, and Schema. It uses the same Smithers
runtime as JSX: steps are persisted in SQLite, completed work is not re-run on
resume, outputs are schema-validated, and dependencies drive scheduling. The
difference is authoring style: every step, approval, sequence, parallel block,
match, branch, loop, worktree, and scope is an ordinary value you can export,
return from a function, or compose with other graph values.
Use JSX for most workflows. Use the Effect API when your workflow lives inside
an Effect service, you want step bodies to return Effect values directly, or
you need a React-free API for generated workflow definitions.
Minimal workflow
Smithers.workflow(opts) returns a typed handle G. Every constructor
(G.step, G.approval, G.sequence, G.parallel, G.match, G.branch,
G.loop, G.worktree, G.scope) returns a graph value. G.from(graph)
finalizes the workflow into something execute-able.
execute() returns an Effect. The success value is the decoded output of the
final graph node: a step output for a single step, the last child for a
sequence, a tuple for a parallel block. If the run stops on an approval or
timer, the success value is the normal RunResult with a waiting status.
Steps and dependencies
Steps are values:input is typed from the workflow’s input schema. The step’s output type is
inferred from the output schema and flows into anything that lists this step
in needs:
Promise, or an
Effect; Smithers decodes the result with the step’s output schema before
writing it.
The step context includes input, dependency values, executionId, stepId,
attempt, iteration, signal, heartbeat(data), and lastHeartbeat.
Control flow
UseG.sequence(...nodes) for ordered work and
G.parallel(...nodes, { maxConcurrency }) for concurrent work. G.parallel
returns a tuple of child results.
G.match(source, { when, then, else }) selects between two statically-known
branches based on a completed step’s output. Both branches are compiled into
the graph; only the matching branch executes.
G.branch({ condition, needs, then, else }) is the same shape but the
predicate runs against an arbitrary needs context, not a single source step.
G.loop({ id, children, until, maxIterations, onMaxReached }) repeats a fragment until the
predicate returns true. Nested loops are not supported. onMaxReached accepts 'fail' or 'return-last' and controls behavior when maxIterations is exceeded; when omitted the loop returns the last iteration’s outputs rather than failing.
Worktrees
G.worktree({ id, path, branch, skipIf, needs, children }) runs children
inside a git worktree. The worktree is created before the children execute and
torn down afterward.
Reuse
Static reuse is just a graph value:G.scope(instanceId, fragment). The compiler applies
instanceId. as a durable ID prefix to every step and approval inside the
fragment. For example, G.scope('api', makeReviewShard(...)) produces step IDs api.read and api.summarize in the database. The same fragment can be mounted under multiple scopes without collision:
Cross-workflow fragments
For graph fragments that need to live across workflows with different inputs, build them withSmithers.fragment(inputSchema):
Smithers.fragment exposes the same constructors as a workflow handle (step,
approval, sequence, parallel, match, branch, loop, worktree,
scope), but no from; fragments are values, not workflows. Compile happens
when they’re mounted into a real workflow:
input type. At runtime the schema is not read or validated; steps receive the host workflow’s input directly. The host workflow’s input type must be assignable to the fragment’s input schema. This is enforced at compile time: TypeScript will error if you mount a fragment whose input schema has fields the host workflow’s input doesn’t satisfy.
Operational notes
- Provide exactly one persistence layer with
Effect.provide(Smithers.sqlite({ filename })). - Keep step IDs stable across releases; use new IDs for materially different work.
- Use
heartbeat()in long-running steps and honorsignalin external calls. - Use
retry,retryPolicy,timeout,skipIf, andcachethe same way you would on JSX tasks (see JSX Task options for the shared option shape). - All graph values support
.pipe(...fns)for future data-last combinators. - Prefer idempotent step bodies. For external side effects, use
executionId,stepId, andattemptwhen constructing idempotency keys. G.matchis graph topology selection: both branches must be statically knowable so durable IDs stay stable across resume. It is not Effect’sMatchmodule (which is runtime value pattern matching).