Skip to main content
Workflow.step is the fundamental building block of durable workflows. Each step’s result is automatically cached in Durable Object storage so that on workflow replay, completed steps are skipped and their cached value is returned directly.

Signature

function step<A, E, R>(
  config: StepConfig<A, E, R>
): Effect.Effect<A, StepErrors<E>, StepRequirements<R>>

Parameters

config
StepConfig
required
Configuration object for the step.

Return type

Returns Effect<A, StepErrors<E>, StepRequirements<R>> where A is the step’s output type and StepErrors<E> is a union of:
StepErrors
union

Serialization requirement

Step results are serialized to JSON and stored in Durable Object storage. The value returned by execute must be JSON-serializable (plain objects, arrays, strings, numbers, booleans, null).
// Non-serializable result (ORM object, class instance) — discard it
yield* Workflow.step({
  name: "Update database",
  execute: updateRecord(id).pipe(Effect.asVoid),
});

// Extract serializable fields from a complex result
yield* Workflow.step({
  name: "Create order",
  execute: createOrder(data).pipe(
    Effect.map((order) => ({ id: order.id, status: order.status }))
  ),
});

Examples

Basic step

const user = yield* Workflow.step({
  name: "Fetch user",
  execute: fetchUser(userId),
});

// user is typed based on what fetchUser returns
console.log(user.email);

Step with retry

yield* Workflow.step({
  name: "Call external API",
  execute: callExternalAPI(),
  retry: {
    maxAttempts: 5,
    delay: "5 seconds",
  },
});

Step with exponential backoff and jitter disabled

import { Backoff } from "@durable-effect/workflow";

yield* Workflow.step({
  name: "Payment processor",
  execute: chargeCard(orderId),
  retry: {
    maxAttempts: 5,
    delay: Backoff.exponential({ base: "1 second", max: "30 seconds" }),
    jitter: false,
  },
});

Step with timeout

yield* Workflow.step({
  name: "Slow third-party API",
  execute: callThirdParty(),
  timeout: "30 seconds",
});

Step with timeout and retry

When combined, the timeout applies to each attempt individually:
yield* Workflow.step({
  name: "Resilient API call",
  execute: callAPI(),
  timeout: "30 seconds",  // each attempt gets 30 seconds
  retry: { maxAttempts: 3 },
  // total max time: 3 attempts × 30 seconds = 90 seconds (plus delays)
});

Selective retry with isRetryable

import { Data } from "effect";

class ValidationError extends Data.TaggedError("ValidationError")<{
  readonly message: string;
}> {}

class NetworkError extends Data.TaggedError("NetworkError")<{
  readonly message: string;
}> {}

yield* Workflow.step({
  name: "Process payment",
  execute: processPayment(paymentId),
  retry: {
    maxAttempts: 5,
    delay: Backoff.presets.standard(),
    isRetryable: (error) => error instanceof NetworkError,
    // ValidationError will not be retried
  },
});

Step with total max duration

yield* Workflow.step({
  name: "Time-bounded operation",
  execute: operation(),
  retry: {
    maxAttempts: 100,
    delay: Backoff.exponential({ base: "1 second" }),
    maxDuration: "5 minutes",
  },
});

Build docs developers (and LLMs) love