Skip to main content
The builder API defines workflows through a typed graph. You call Smithers.workflow(...).build($) and use $ to create steps and control-flow nodes.

Smithers.workflow()

Creates a workflow definition:
const MyWorkflow = Smithers.workflow({
  name: "my-workflow",
  input: Input,
}).build(($) => {
  // define steps and return a control-flow node
});
OptionTypeDescription
namestringUnique workflow name
inputSchema.ClassInput schema

$.step()

Defines a single executable step:
const analyze = $.step("analyze", {
  output: Analysis,
  run: ({ input }) =>
    Effect.gen(function* () {
      const coder = yield* Coder;
      return yield* coder.analyze(input.description);
    }),
});

Step Options

OptionTypeRequiredDescription
outputModel.ClassyesThe persisted output model
run(ctx) => Effect<Output>yesThe step body
needsRecord<string, StepHandle>noUpstream dependencies
retryanynoRetry configuration — accepts an Effect Schedule or a plain number (retry count)
retryPolicyRetryPolicynoDeclarative retry policy with backoff strategy ({ backoff?: "fixed" | "linear" | "exponential", initialDelayMs?: number })
timeoutDurationInputnoStep timeout
cacheCachePolicynoCache configuration
skipIf(ctx) => booleannoCondition to skip

Run Context

The run function receives:
run: ({ input, executionId, stepId, attempt, signal, iteration, ...needs }) => ...
  • input — validated workflow input
  • Spread needs — resolved dependency outputs as named fields
  • executionId — durable execution id
  • stepId — logical step id
  • attempt — current attempt number (starts at 1)
  • signalAbortSignal for timeout/cancellation
  • iteration — loop iteration (0 for non-looped steps)

Step Handle

$.step() returns a step handle — a typed reference used in needs and control-flow nodes:
const analyze = $.step("analyze", { output: Analysis, run: ... });

const fix = $.step("fix", {
  output: Fix,
  needs: { analysis: analyze },  // typed dependency
  run: ({ analysis }) => ...,     // analysis is typed as Analysis
});

$.sequence()

Executes nodes sequentially:
return $.sequence(analyze, fix, deploy);
Steps run in order. Each step waits for the previous to complete.

$.parallel()

Executes nodes concurrently:
return $.parallel(researchWeb, researchDocs, researchCode);

With Concurrency Limit

return $.parallel(task1, task2, task3, task4, {
  maxConcurrency: 2,
});

$.loop()

Repeats nodes until a condition is met:
const review = $.step("review", {
  output: Review,
  run: ({ draft }) => ...,
});

const revise = $.step("revise", {
  output: Draft,
  needs: { review },
  run: ({ review }) => ...,
  skipIf: ({ review }) => review.approved,
});

return $.loop({
  id: "review-loop",
  children: $.sequence(review, revise),
  until: (outputs) => outputs.review.approved,
  maxIterations: 5,
  onMaxReached: "return-last",
});

Loop Options

OptionTypeRequiredDescription
idstringnoLoop identifier
childrennodeyesThe nodes to repeat
until(outputs) => booleanyesStop condition
maxIterationsnumbernoSafety limit (default: 5)
onMaxReached"fail" | "return-last"noBehavior at limit

$.approval()

Creates a durable approval gate:
const approveDeploy = $.approval("approve-deploy", {
  needs: { build },
  request: ({ build }) => ({
    title: `Deploy ${build.version}?`,
    summary: `Commit ${build.commitSha} passed all checks.`,
  }),
  onDeny: "fail",
});

Approval Options

OptionTypeRequiredDescription
needsRecord<string, StepHandle>noDependencies
request(needs) => { title, summary }yesRequest payload
onDeny"fail" | "continue" | "skip"noDenial behavior
The approval gate resolves to an ApprovalDecision:
type ApprovalDecision = {
  readonly approved: boolean;
  readonly note: string | null;
  readonly decidedBy: string | null;
  readonly decidedAt: string | null;
};

$.match()

Branches based on a value:
return $.match(classify, {
  when: (result) => result.severity === "high",
  then: () => $.step("escalate", { output: Escalation, run: ... }),
  else: () => $.step("auto-fix", { output: AutoFix, run: ... }),
});
Also works with approval decisions:
return $.match(approval, {
  when: (decision) => decision.approved,
  then: () => $.step("publish", { output: Published, run: ... }),
  else: () => $.step("record-rejection", { output: Rejection, run: ... }),
});

$.component()

References a reusable workflow fragment:
const ReviewCycle = Smithers.component({
  name: "ReviewCycle",
  params: { content: "string", reviewer: "string" },
}).build(($, params) => {
  const review = $.step("review", {
    output: Review,
    run: ({ }) => ...,
  });
  const revise = $.step("revise", {
    output: Revised,
    needs: { review },
    run: ({ review }) => ...,
  });
  return $.sequence(review, revise);
});

// Usage
const techReview = $.component("tech-review", ReviewCycle, {
  content: draft,
  reviewer: "senior engineer",
});

Composition

All primitives compose freely:
return $.sequence(
  analyze,
  $.parallel(
    researchWeb,
    researchDocs,
  ),
  $.loop({
    children: $.sequence(review, revise),
    until: (o) => o.review.approved,
    maxIterations: 3,
  }),
  approveDeploy,
  deploy,
);

Smithers.loadToon()

Loads a .toon file and returns a workflow that can be executed like any builder workflow:
const MyWorkflow = Smithers.loadToon("./workflows/my-workflow.toon");

const result = await Effect.runPromise(
  MyWorkflow.execute(new Input({ ... })).pipe(
    Effect.provide(AppLive),
  ),
);
ParamTypeDescription
pathstringFile path to the .toon workflow definition
Returns a BuiltSmithersWorkflow with an execute method that accepts the workflow input and optional execution options.

Executing a Workflow

const result = await Effect.runPromise(
  MyWorkflow.execute(new Input({ ... })).pipe(
    Effect.provide(AppLive),
  ),
);
The workflow requires all services declared in step run functions to be provided through the Layer stack.

Next Steps

  • Context — Detailed step context reference.
  • Services — Defining and providing Effect services.
  • TOON Nodes — The declarative equivalent of builder primitives.