Skip to main content
In the builder API, a step does not read from a global workflow context object. Instead, Smithers passes a typed run context directly into the step’s run function. That context is made from:
  • validated workflow input
  • resolved dependency outputs from needs
  • execution metadata such as the execution id and attempt number
  • cancellation and timeout state
This is a deliberate shift away from ambient lookup APIs like ctx.output("...", ...).

The Basic Shape

A step looks like this:
const report = $.step("report", {
  output: Report,
  needs: { research },
  run: ({ input, research, executionId, attempt, signal }) =>
    Effect.gen(function* () {
      const writer = yield* Writer;
      return yield* writer.write({
        topic: input.topic,
        summary: research.summary,
        attempt,
        signal,
      });
    }),
});
The run argument is the step context.

Step Context Interface

The source type is named BuilderStepContext (also referred to as StepContext in some older docs). Conceptually, Smithers provides something like:
type BuilderStepContext<Input, Needs> = {
  readonly input: Input;
  readonly executionId: string;
  readonly stepId: string;
  readonly attempt: number;
  readonly signal: AbortSignal;
  readonly iteration: number;
  readonly loop: { iteration: number };
} & Needs;
If a step declares:
needs: { analysis, review }
then the run function receives:
run: ({ input, analysis, review, executionId, stepId, attempt, signal }) => ...
The dependency outputs are regular typed values, not table lookups.

input

input is the validated workflow input defined at the workflow boundary:
class Input extends Schema.Class<Input>("Input")({
  description: Schema.String,
}) {}
Every step in the workflow can read it:
run: ({ input }) =>
  Effect.succeed(
    new Analysis({
      summary: `Analyzing ${input.description}`,
      severity: "high",
    }),
  )

Dependency Outputs

Dependencies are the main source of typed step-to-step dataflow.
const analyze = $.step("analyze", {
  output: Analysis,
  run: ({ input }) => ...,
});

const fix = $.step("fix", {
  output: Fix,
  needs: { analysis: analyze },
  run: ({ analysis }) => ...,
});
In fix, the analysis value is already resolved and typed as Analysis. This has two important consequences:
  • the dependency edge is explicit in the graph
  • the step body does not need to know how persistence works

Execution Metadata

Smithers exposes a small amount of orchestration metadata in every step context.

executionId

The durable id for the workflow run. Useful for logging, tracing, and correlating external side effects.

stepId

The logical id of the currently executing step.

attempt

The current attempt number for this step. Starts at 1 and increments on each retry.

signal

An AbortSignal that fires when:
  • the step times out
  • the workflow is interrupted
  • the runtime is shutting down

iteration

For looped workflows, this identifies the current durable iteration. For non-loop steps, it is usually 0.

loop

An object with loop metadata: { iteration: number }. Provides structured access to the current loop iteration. This is available on every step context; for non-looped steps, loop.iteration is 0.

No Hook API

There is no useCtx() in the builder design. That is intentional:
  • workflows are not React components
  • steps do not re-render to discover new dependencies
  • runtime dataflow should stay explicit in the builder graph
If you want reusable workflow fragments, use regular functions that accept step handles or builder helpers, not hooks.

Services Do Not Belong in Step Context

The step context is for workflow data, not infrastructure dependencies. To access services, use normal Effect dependency injection:
run: ({ analysis }) =>
  Effect.gen(function* () {
    const coder = yield* Coder;
    return yield* coder.fix(analysis);
  })
This keeps two separate concerns clean:
  • Smithers passes workflow data
  • Effect provides runtime capabilities

Multiple Dependencies

Steps can depend on more than one upstream value:
const summary = $.step("summary", {
  output: Summary,
  needs: {
    analysis,
    review,
    tests,
  },
  run: ({ input, analysis, review, tests }) =>
    Effect.succeed(
      new Summary({
        title: input.ticketId,
        body: `${analysis.summary}\n\n${review.feedback}\n\nTests: ${tests.status}`,
      }),
    ),
});
All dependency outputs are available as named parameters, and TypeScript keeps them aligned with the step handles you declared in needs.

Historical Access

The old API exposed helpers like latest() and outputMaybe() for global output lookup. The builder design should avoid making that the default. The preferred rule is:
  • if a step needs an upstream value, declare it in needs
  • if a workflow needs historical or ad hoc reads, use an explicit repository or workflow-state service
For example:
run: ({ executionId }) =>
  Effect.gen(function* () {
    const history = yield* WorkflowHistory;
    const reviews = yield* history.listStepOutputs(executionId, "review");
    // ...
  })
That keeps “normal dataflow” and “advanced state inspection” separate.

Suggested Mental Model

Think of a Smithers step as a typed function:
({ input, ...needs, executionId, attempt, signal }) => Effect<Output>
That is all most steps should need. If you find yourself wanting a large mutable workflow context, it usually means one of two things:
  1. a missing dependency should be declared in needs
  2. the workflow needs a repository or service for advanced queries

Next Steps