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?).
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.
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:
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
| Option | Type | Default | Description |
|---|---|---|---|
retries | number | 0 | Number of retry attempts after the first failure. |
retryDelayMs | number | 0 | Delay in milliseconds before retries. |
backoffFactor | "fixed" | "linear" | "exponential" | "fixed" behavior when omitted | Strategy used to compute retry delay growth. |
jitter | boolean | false | Adds randomness to retry delays. |
maxRetryDelayMs | number | undefined | Caps the computed retry delay. |
timeoutMs | number | undefined | Maximum allowed runtime for a single attempt. |
compensate | (ctx) => void | Promise<void> | undefined | Undo logic executed if the flow fails after this step completed. |
What happens when a step fails
If a step throws:
- Orchestrix records the attempt
- retries the step if retries are configured
- marks the step as failed if retries are exhausted
- stops normal forward execution
- 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.