Workflow.step() is the fundamental durability primitive. Each step:
- Executes an Effect and stores the result in Durable Object storage
- Returns the cached result on subsequent runs without re-executing the effect
- Must return a JSON-serializable value
Step config
Unique name for this step within the workflow. Used as the cache key — two steps with the same name share their cached result.
The effect to run. Its result is cached after the first successful execution.
Basic usage
import { Effect } from "effect";
import { Workflow } from "@durable-effect/workflow";
const myWorkflow = Workflow.make((orderId: string) =>
Effect.gen(function* () {
const order = yield* Workflow.step({
name: "Fetch order",
execute: fetchOrder(orderId),
});
const user = yield* Workflow.step({
name: "Fetch user",
execute: fetchUser(order.userId),
});
yield* Workflow.step({
name: "Process order",
execute: processOrder(order, user),
});
})
);
JSON-serializable results
Step results are stored as JSON. If your effect returns a value that cannot be serialized — a class instance, an ORM model, a function — you must transform it before it reaches the step boundary.
Discard the result with Effect.asVoid:
yield* Workflow.step({
name: "Update database",
execute: updateRecord(id).pipe(Effect.asVoid),
});
Extract only the serializable fields:
yield* Workflow.step({
name: "Create order",
execute: createOrder(data).pipe(
Effect.map((order) => ({ id: order.id, status: order.status }))
),
});
If a step returns a non-serializable value, the result cannot be stored and the workflow will fail on resume when it tries to replay the cached result.
Replaying cached results
When a workflow resumes after a sleep or restart, steps that already completed return their cached result immediately — the execute effect is not called again.
const result = yield* Workflow.step({
name: "Slow computation",
execute: expensiveOperation(), // Only runs once, ever
});
// On replay: returns cached result without calling expensiveOperation()
This is what makes workflows durable: completed work is never repeated.
Step with retry
yield* Workflow.step({
name: "External API call",
execute: callExternalAPI(),
retry: { maxAttempts: 3, delay: "5 seconds" },
});
Step with timeout and retry
yield* Workflow.step({
name: "Resilient call",
execute: riskyOperation(),
timeout: "30 seconds",
retry: {
maxAttempts: 3,
delay: Backoff.exponential({ base: "1 second", max: "30 seconds" }),
},
});
The timeout applies to each individual attempt. With 3 retries and a 30-second timeout, the maximum total time for the step is 90 seconds plus any retry delays.
Constraints
Steps cannot be nested. Workflow.step() and Workflow.sleep() may only be called at the top level of a workflow effect — not inside another step’s execute effect. This is enforced at compile time via the WorkflowLevel context tag.
// Correct: steps at workflow level
yield* Workflow.step({ name: "Fetch", execute: fetchData() });
yield* Workflow.sleep("1 hour");
yield* Workflow.step({ name: "Process", execute: processData() });
// Incorrect: sleep inside a step's execute - compile error
yield* Workflow.step({
name: "Bad",
execute: Effect.gen(function* () {
yield* Workflow.sleep("1 hour"); // Error: WorkflowLevel not available here
}),
});