Effect, Layer, and Schema. It shares the JSX
runtime (SQLite persistence, no rerun of completed work on resume,
schema-validated outputs, dependency-driven scheduling), differing only in
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 others.
Use JSX for most workflows; use the Effect API inside an Effect service, when
step bodies should return Effect values directly, or for a React-free API
over 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 an executable one.
execute() returns an Effect whose success value is the decoded output of
the final graph node: a step’s own output, a sequence’s last child, a
parallel block’s tuple. If it stops on an approval or timer instead, the
value is a RunResult with 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 output and flows into anything listing this step in needs:
Promise, or
Effect; Smithers decodes the result with the step’s output schema before
writing it.
Step context: input, dependency values, executionId, stepId, attempt,
iteration, signal, heartbeat(data), and lastHeartbeat.
Control flow
G.sequence(...nodes) runs ordered work; G.parallel(...nodes, { maxConcurrency })
runs concurrent work and 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 compile into the graph, but
only the matching one executes.
G.branch({ condition, needs, then, else }) is G.match for an arbitrary
needs context instead of a single source step.
G.loop({ id, children, until, maxIterations, onMaxReached }) repeats a fragment
(not nestable) until the predicate returns true. onMaxReached
('fail' or 'return-last') governs maxIterations overflow; the default
returns the last iteration’s outputs rather than failing.
Worktrees
G.worktree({ id, path, branch, skipIf, needs, children }) runs children
inside a git worktree, created beforehand and torn down after.
Reuse
Static reuse is a graph value:G.scope(instanceId, fragment): the compiler prefixes
every step and approval ID in the fragment with instanceId., so
G.scope('api', makeReviewShard(...)) produces api.read and api.summarize.
The same fragment mounts under multiple scopes without collision:
Cross-workflow fragments
Fragments that need to live across workflows with different inputs are built 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, compiled only once mounted into
a real workflow:
input type; at runtime it’s never read or validated, since steps receive the host workflow’s input directly. That input must be assignable to the fragment’s schema, or TypeScript errors at compile time when mounting a fragment with fields the host 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, andcacheas 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.matchselects graph topology: both branches must be statically knowable so durable IDs stay stable across resume, unlike Effect’sMatchmodule, which does runtime value pattern matching.