Skip to main content
The Effect API is a lower-level authoring surface for teams already modeling application logic with 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:
Step IDs are durable: changing one creates a new task and leaves the old persisted output behind. A step may return a plain value, 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:
Parameterized reuse is a function returning a graph value:
Multi-mount reuse is 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 with Smithers.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:
A fragment’s input schema lets TypeScript infer each step’s 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 honor signal in external calls.
  • Use retry, retryPolicy, timeout, skipIf, and cache as 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, and attempt when constructing idempotency keys.
  • G.match selects graph topology: both branches must be statically knowable so durable IDs stay stable across resume, unlike Effect’s Match module, which does runtime value pattern matching.