Skip to content

Steps

A Step is a single unit of work within a flow.

Good steps are small, understandable, and meaningful from a business perspective. A step should represent an action you can name clearly, reason about in isolation, and inspect later in the flow result.

What a step is responsible for

A step usually does one of these things:

  • validate or transform data
  • call an external service
  • persist something
  • prepare data for later steps
  • produce a side effect that might need compensation

If a step is doing too many unrelated things, it becomes hard to reason about retries, timeouts, and rollback behavior.

Defining a step

Steps are defined with .step(name, fn, options?).

ts
flow.step("calculate-total", (ctx) => {
  const items = ctx.input.items;
  const total = items.reduce((sum, item) => sum + item.price, 0);
  ctx.set("total", total);
});

Each step must have a unique name within the flow.

Step function

The execution function receives the Flow Context and can be synchronous or asynchronous.

ts
flow.step("save-order", async (ctx) => {
  const order = await db.orders.insert(ctx.input);
  ctx.set("orderId", order.id);
});

The function should focus on the work itself. Execution policies like retries and timeouts belong in the step options.

Step options

The third argument configures execution behavior:

ts
flow.step("flaky-operation", async (ctx) => {
  // ...
}, {
  retries: 3,
  retryDelayMs: 500,
  backoffFactor: "exponential",
  jitter: true,
  maxRetryDelayMs: 5000,
  timeoutMs: 5000,
  compensate: async (ctx) => {
    // Undo work if a later step fails
  }
});

Options Reference

OptionTypeDefaultDescription
retriesnumber0Number of retry attempts after the first failure.
retryDelayMsnumber0Delay in milliseconds before retries.
backoffFactor"fixed" | "linear" | "exponential""fixed" behavior when omittedStrategy used to compute retry delay growth.
jitterbooleanfalseAdds randomness to retry delays.
maxRetryDelayMsnumberundefinedCaps the computed retry delay.
timeoutMsnumberundefinedMaximum allowed runtime for a single attempt.
compensate(ctx) => void | Promise<void>undefinedUndo logic executed if the flow fails after this step completed.

What happens when a step fails

If a step throws:

  1. Orchestrix records the attempt
  2. retries the step if retries are configured
  3. marks the step as failed if retries are exhausted
  4. stops normal forward execution
  5. starts compensation for previously completed steps

This makes step design important. A step is not just code. It is also the unit of failure semantics.

What happens when a step times out

Timeout applies per attempt.

That means a step with timeoutMs: 5000 and retries: 2 may attempt execution multiple times, with each attempt allowed to run for up to five seconds.

What makes a good step

A good step usually has:

  • one clear purpose
  • a stable name
  • a reasonable retry policy if it touches unstable infrastructure
  • a compensation function if it creates a durable side effect

Common mistakes

  • Combining validation, persistence, and notification in one step.
  • Retrying permanent business errors.
  • Forgetting compensation for a step that reserves or charges something.
  • Storing too much unrelated data in context from inside a step.

Best Practices

  • Keep steps small and explicit.
  • Prefer one business action per step.
  • Make side-effecting steps idempotent when possible.
  • Use context to pass only the data needed by later steps or compensation.

Released under the MIT License.