Skip to content

Step

Checkpointed results

A step executes the code you provide and checkpoints the result. On replay, the SDK returns the checkpointed result rather than re-running the code inside the step.

Use steps to encapsulate any code that should not re-run once it has completed.

Wrapping non-deterministic code in steps is the primary way you ensure that your durable execution is deterministic. Non-deterministic code includes fetching the current time, generating a random number or UUID, causing side-effects such as writing to disk, or calling an API that might return a different result on different calls.

When you encapsulate such code in a step it becomes deterministic in your durable execution because the step doesn’t generate different results on replay.

If a step fails during execution, it retries according to its configured retry strategy. The step will checkpoint the last error after exhausting all retry attempts.

import {
  DurableContext,
  StepContext,
  withDurableExecution,
} from "@aws/durable-execution-sdk-js";

async function addNumbers(ctx: StepContext, a: number, b: number): Promise<number> {
  return a + b;
}

export const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    const result = await context.step("add_numbers", (ctx) => addNumbers(ctx, 5, 3));
    return result;
  },
);
from aws_durable_execution_sdk_python import (
    DurableContext,
    StepContext,
    durable_execution,
    durable_step,
)


@durable_step
def add_numbers(step_context: StepContext, a: int, b: int) -> int:
    return a + b


@durable_execution
def handler(event: dict, context: DurableContext) -> int:
    result = context.step(add_numbers(5, 3))
    return result
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.StepContext;

public class AddNumbersExample extends DurableHandler<Object, Integer> {
    @Override
    public Integer handleRequest(Object input, DurableContext context) {
        int result = context.step("add_numbers", Integer.class,
            (StepContext ctx) -> 5 + 3);
        return result;
    }
}
package main

import "github.com/aws/aws-durable-execution-sdk-go/durable"

func addNumbers(ctx durable.StepContext, a, b int) (int, error) {
    return a + b, nil
}

func handler(ctx durable.Context, event map[string]any) (int, error) {
    return durable.Step(ctx, "add_numbers", func(sc durable.StepContext) (int, error) {
        return addNumbers(sc, 5, 3)
    })
}

func main() {
    durable.Start(handler)
}
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;

public class AddNumbersExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<object, int>(Workflow, input, context);

    private async Task<int> Workflow(object input, IDurableContext ctx)
    {
        int result = await ctx.StepAsync(
            async (_, _) => AddNumbers(5, 3),
            name: "add_numbers");
        return result;
    }

    private static int AddNumbers(int a, int b) => a + b;
}

Method signature

step

import { DurableContext, StepConfig, StepFunc } from "@aws/durable-execution-sdk-js";

// Sync signature (unnamed)
context.step(fn: StepFunc<T>, config?: StepConfig<T>): DurablePromise<T>

// Named signature
context.step(name: string | undefined, fn: StepFunc<T>, config?: StepConfig<T>): DurablePromise<T>

Parameters:

  • name (optional) A name for the step. Pass undefined to omit.
  • fn A function that receives a StepContext and returns a Promise<T>.
  • config (optional) A StepConfig<T> object.

Returns: DurablePromise<T>. Use await to get the result.

Throws: StepError (a DurableOperationError subclass) wrapping the original error after retries are exhausted. For an at-most-once step that Lambda interrupted before the SDK checkpointed the result, the SDK throws a StepError whose cause.name equals "StepInterruptedError".

from aws_durable_execution_sdk_python import StepContext
from aws_durable_execution_sdk_python.config import StepConfig


def step(
    func: Callable[[StepContext], T],
    name: str | None = None,
    config: StepConfig | None = None,
) -> T: ...

Parameters:

  • func A callable that receives a StepContext and returns T.
  • name (optional) A name for the step. Defaults to the function's name when using @durable_step.
  • config (optional) A StepConfig object.

Returns: T, the return value of func.

Raises: StepError (a DurableOperationError subclass) wrapping the original exception after retries are exhausted. An at-most-once step interrupted before it checkpointed also surfaces as StepError, with error_type recording StepInterruptedError. A permanent serializer failure raises SerDesError.

// Sync (blocks until complete)
<T> T step(String name, Class<T> resultType, Function<StepContext, T> func)
<T> T step(String name, Class<T> resultType, Function<StepContext, T> func, StepConfig config)

// Async (returns a DurableFuture)
<T> DurableFuture<T> stepAsync(String name, Class<T> resultType, Function<StepContext, T> func)
<T> DurableFuture<T> stepAsync(String name, Class<T> resultType, Function<StepContext, T> func, StepConfig config)

Parameters:

  • name (required) A name for the step.
  • resultType The Class<T> or TypeToken<T> for deserialization.
  • func A Function<StepContext, T> to execute.
  • config (optional) A StepConfig object.

Returns: T (sync) or DurableFuture<T> (async via stepAsync()).

Throws: The original exception re-thrown after deserialization if possible, otherwise StepFailedException. StepInterruptedException if an at-most-once step was interrupted.

// Sync
func Step[O any](ctx Context, name string, fn func(StepContext) (O, error), opts ...StepOption) (O, error)

// Async
func StepAsync[O any](ctx Context, name string, fn func(StepContext) (O, error), opts ...StepOption) *Future[O]

Parameters:

  • ctx (required) The durable Context, always the first argument.
  • name (required) A name for the step. Pass "" to omit it.
  • fn A func(durable.StepContext) (O, error) to execute. The result type O is inferred from its return value.
  • opts (optional) Zero or more StepOption values.

Returns: (O, error) from Step, or *durable.Future[O] from StepAsync. Read the async result with fut.Result(ctx).

Errors: *durable.StepError when the step body fails and the retry strategy stops. Its Err field is a stand-in rebuilt from ErrorType and Message, the same on the first run and on replay. So match on ErrorType, not with errors.As against the body's own error type. An interrupted AtMostOncePerRetry step that the retry strategy stops returns a *durable.StepError whose ErrorType is "StepInterruptedError". errors.As reaches a *durable.StepInterruptedError through it. A result the serializer cannot convert fails the step at once, without a retry. The *durable.StepError then has ErrorType "SerdesError". An invalid WithStepSubType value returns a plain error before the step runs.

// Returns a value
Task<T> StepAsync<T>(
    Func<IStepContext, CancellationToken, Task<T>> func,
    string? name = null,
    StepConfig? config = null,
    CancellationToken cancellationToken = default);

// Returns no value
Task StepAsync(
    Func<IStepContext, CancellationToken, Task> func,
    string? name = null,
    StepConfig? config = null,
    CancellationToken cancellationToken = default);

Parameters:

  • func A function that receives an IStepContext and a CancellationToken and returns a Task<T> (or Task for the no-value overload).
  • name (optional) A name for the step. Omit it to infer one from the call site.
  • config (optional) A StepConfig object.
  • cancellationToken (optional) A token linked with the SDK's workflow-shutdown signal, forwarded to func.

Returns: Task<T>, or Task for the no-value overload. Use await to get the result.

Throws: The exception thrown by the step body after retries are exhausted. An OperationCanceledException that propagates via the linked token is treated as cancellation, not a step failure. See Cancellation.

StepConfig

interface StepConfig<T> {
  retryStrategy?: (error: Error, attemptCount: number) => RetryDecision;
  semantics?: StepSemantics;
  serdes?: Serdes<T>;
}

Parameters:

  • retryStrategy (optional) A function returning a RetryDecision. Use createRetryStrategy() to build one. See Retry strategies.
  • semantics (optional) StepSemantics.AtLeastOncePerRetry (default) or StepSemantics.AtMostOncePerRetry.
  • serdes (optional) Custom Serdes<T> for the step result. See Serialization.
@dataclass(frozen=True)
class StepConfig:
    retry_strategy: Callable[[Exception, int], RetryDecision] | None = None
    step_semantics: StepSemantics = StepSemantics.AT_LEAST_ONCE_PER_RETRY
    serdes: SerDes | None = None

Parameters:

  • retry_strategy (optional) A callable returning a RetryDecision. Use create_retry_strategy() to build one. See Retry strategies.
  • step_semantics (optional) StepSemantics.AT_LEAST_ONCE_PER_RETRY (default) or StepSemantics.AT_MOST_ONCE_PER_RETRY.
  • serdes (optional) Custom SerDes for the step result. See Serialization.
StepConfig.builder()
    .retryStrategy(RetryStrategy)          // optional
    .semanticsPerRetry(StepSemantics)      // optional
    .serDes(SerDes)                        // optional
    .build()

Parameters:

  • retryStrategy (optional) A RetryStrategy instance. Use RetryStrategies.exponentialBackoff() to build one. See Retry strategies.
  • semanticsPerRetry (optional) StepSemantics.AT_LEAST_ONCE_PER_RETRY (default) or StepSemantics.AT_MOST_ONCE_PER_RETRY.
  • serDes (optional) Custom SerDes for the step result. See Serialization.

Pass options to Step.

func WithRetry(s RetryStrategy) StepOption
func WithSemantics(s StepSemantics) StepOption
func WithStepSerdes(s Serdes) StepOption
func WithStepSubType(subType string) StepOption

Parameters:

  • WithRetry (optional) A RetryStrategy. Build one with durable.NewRetryStrategy, durable.ExponentialBackoff, or durable.LinearBackoff. The default is durable.ExponentialBackoff(). It makes 6 attempts in total, with a 5-second first delay that doubles on each retry up to 60 seconds, and full jitter. So a step retries unless you pass durable.NoRetry(). See Retry strategies.
  • WithSemantics (optional) durable.AtLeastOncePerRetry (default) or durable.AtMostOncePerRetry.
  • WithStepSerdes (optional) A custom Serdes for the step result. The default is the handler-level serializer, which is JSON unless you set durable.WithSerdes or call durable.ConfigureSerdes. See Serialization.
  • WithStepSubType (optional) The operation subtype the step records in the checkpoint. The default is "Step", and an empty string selects it. A subtype has 1 to 32 characters from A-Z, a-z, 0-9, hyphen, and underscore. The subtypes the SDK records for its own operations are reserved. An invalid value makes Step return an error. The subtype is part of the step's replay identity, so a different subtype on replay returns a *durable.NonDeterministicExecutionError.
public sealed class StepConfig
{
    public IRetryStrategy? RetryStrategy { get; set; }  // null = no retry
    public StepSemantics Semantics { get; set; }        // default AtLeastOncePerRetry
}

Parameters:

  • RetryStrategy (optional) An IRetryStrategy. When null (default), failures are not retried. Use the RetryStrategy factory (for example RetryStrategy.Exponential(...)) to build one. See Retry strategies.
  • Semantics (optional) StepSemantics.AtLeastOncePerRetry (default) or StepSemantics.AtMostOncePerRetry.

The step result is serialized with the ILambdaSerializer registered on ILambdaContext.Serializer; there is no per-step serializer. See Serialization.

StepContext

interface StepContext {
  logger: DurableContextLogger;
}
  • logger A logger enriched with execution context metadata. See Logging.
@dataclass(frozen=True)
class StepContext:
    logger: LoggerInterface
    attempt: int          # current attempt, 1-based
  • logger A logger enriched with execution context metadata. See Logging.
  • attempt The current attempt number, starting at 1 for the first execution.
interface StepContext {
    DurableLogger getLogger();
    int getAttempt();      // current retry attempt, 1-based
}
  • getLogger() A logger enriched with execution context metadata. See Logging.
  • getAttempt() The current retry attempt number (1-based).

Java StepContext does not expose replay state. Use DurableContext.isReplaying() in orchestration code outside the step function.

type StepContext interface {
    context.Context
    Logger() *slog.Logger
    Attempt() int
}
  • Logger() Returns the step's *slog.Logger from the standard library log/slog. See Logging.
  • Attempt() The 1-based attempt number of the current execution.

StepContext embeds context.Context, so you can pass it directly to AWS SDK calls. It exposes no durable operations and no operation ID.

public interface IStepContext
{
    ILogger Logger { get; }
    int AttemptNumber { get; }   // current retry attempt, 1-based
    string OperationId { get; }
}
  • Logger A logger enriched with execution context metadata. See Logging.
  • AttemptNumber The current retry attempt number (1-based).
  • OperationId The deterministic operation ID for this step.

StepSemantics

enum StepSemantics {
  AtLeastOncePerRetry = "AT_LEAST_ONCE_PER_RETRY",
  AtMostOncePerRetry  = "AT_MOST_ONCE_PER_RETRY",
}
  • AtLeastOncePerRetry (default) Re-executes the step if the function replays before the SDK checkpoints the result. Safe for idempotent operations.
  • AtMostOncePerRetry Executes the step at most once per retry attempt. If the function replays before the SDK checkpoints the result, the SDK skips the step and throws a StepError whose cause.name equals "StepInterruptedError". Use for operations with side effects.
class StepSemantics(Enum):
    AT_LEAST_ONCE_PER_RETRY = "AT_LEAST_ONCE_PER_RETRY"
    AT_MOST_ONCE_PER_RETRY  = "AT_MOST_ONCE_PER_RETRY"
  • AT_LEAST_ONCE_PER_RETRY (default) Re-execute the step if the function replays before the result has checkpointed. Safe for idempotent operations.
  • AT_MOST_ONCE_PER_RETRY Execute the step at most once per retry attempt. If the function replays before the result has checkpointed, the SDK skips the step and raises StepInterruptedError. Use for operations with side effects.
enum StepSemantics {
    AT_LEAST_ONCE_PER_RETRY,
    AT_MOST_ONCE_PER_RETRY
}
  • AT_LEAST_ONCE_PER_RETRY (default) Re-executes the step if the function replays before the result is checkpointed. Safe for idempotent operations.
  • AT_MOST_ONCE_PER_RETRY Executes the step at most once per retry attempt. If the function replays before the result is checkpointed, the SDK skips the step and throws StepInterruptedException. Use for operations with side effects.
type StepSemantics int

const (
    AtLeastOncePerRetry StepSemantics = iota
    AtMostOncePerRetry
)
  • AtLeastOncePerRetry (default) Re-executes a step whose previous invocation was interrupted before it recorded an outcome. Safe for idempotent operations.
  • AtMostOncePerRetry Never re-executes an interrupted attempt. The SDK passes a *durable.StepInterruptedError to the retry strategy as the failed attempt's error, so the interruption consumes one attempt. If the strategy retries, the next attempt runs the step body after the retry delay. If the strategy stops, Step returns a *durable.StepError whose ErrorType is "StepInterruptedError". Use for operations with side effects.
public enum StepSemantics
{
    AtLeastOncePerRetry,
    AtMostOncePerRetry
}
  • AtLeastOncePerRetry (default) Re-executes the step if the Lambda is re-invoked before the result is checkpointed. Safe for idempotent operations.
  • AtMostOncePerRetry Executes the step at most once per retry attempt. If the Lambda is re-invoked before the result is checkpointed, the SDK skips re-execution. Use for operations with side effects.

The Step's function

A step function receives a StepContext as its first parameter.

Pass any async function directly.

import {
  DurableContext,
  StepContext,
  withDurableExecution,
} from "@aws/durable-execution-sdk-js";

async function validateOrder(ctx: StepContext, orderId: string): Promise<object> {
  return { order_id: orderId, valid: true };
}

export const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    const orderId = event.order_id;
    const validation = await context.step("validate_order", (ctx) =>
      validateOrder(ctx, orderId),
    );
    return validation;
  },
);

Step functions are async. await the result of context.step().

Use the @durable_step decorator. It automatically uses the function's name as the step name. Step functions must be synchronous.

from aws_durable_execution_sdk_python import (
    DurableContext,
    StepContext,
    durable_execution,
    durable_step,
)
from aws_durable_execution_sdk_python.config import StepConfig
from aws_durable_execution_sdk_python.retries import (
    RetryStrategyConfig,
    create_retry_strategy,
)


@durable_step
def validate_order(step_context: StepContext, order_id: str) -> dict:
    return {"order_id": order_id, "valid": True}


@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
    order_id = event["order_id"]
    validation = context.step(validate_order(order_id))
    return validation

Pass a lambda or method reference directly. Step functions are synchronous. Use stepAsync() to get a DurableFuture<T> you can compose with other async operations.

import java.util.Map;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.StepContext;

public class ValidateOrderExample extends DurableHandler<Map<String, Object>, Map<String, Object>> {
    @Override
    public Map<String, Object> handleRequest(Map<String, Object> event, DurableContext context) {
        String orderId = (String) event.get("order_id");

        Map<String, Object> validation = context.step("validate_order", Map.class,
            (StepContext ctx) -> Map.of("order_id", orderId, "valid", true));

        return validation;
    }
}

A step body is an ordinary synchronous func(durable.StepContext) (O, error). Use durable.StepAsync to get a *durable.Future[O].

package main

import "github.com/aws/aws-durable-execution-sdk-go/durable"

type OrderEvent struct {
    OrderID string `json:"order_id"`
}

type Validation struct {
    OrderID string `json:"order_id"`
    Valid   bool   `json:"valid"`
}

func validateOrder(ctx durable.StepContext, orderID string) (Validation, error) {
    return Validation{OrderID: orderID, Valid: true}, nil
}

func handler(ctx durable.Context, event OrderEvent) (Validation, error) {
    return durable.Step(ctx, "validate_order", func(sc durable.StepContext) (Validation, error) {
        return validateOrder(sc, event.OrderID)
    })
}

func main() {
    durable.Start(handler)
}

Pass an async (ctx, ct) => ... lambda directly. Step bodies are async; await the result of ctx.StepAsync().

using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;

public class ValidateOrderExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<OrderEvent, Validation>(Workflow, input, context);

    private async Task<Validation> Workflow(OrderEvent input, IDurableContext ctx)
    {
        Validation validation = await ctx.StepAsync(
            async (_, _) => ValidateOrder(input.OrderId),
            name: "validate_order");
        return validation;
    }

    private static Validation ValidateOrder(string orderId) => new(orderId, Valid: true);
}

public record OrderEvent(string OrderId);
public record Validation(string OrderId, bool Valid);

Anonymous step functions

You can also use inline lambdas.

import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js";

export const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    // Lambda without a name — no automatic name
    const result = await context.step(async () => "some value");
    return result;
  },
);

If you use an anonymous function it will not automatically get named like the @durable_step decorator does.

result = context.step(lambda _: "some value", name="my_step")
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.StepContext;

public class LambdaStepNoNameExample extends DurableHandler<Object, String> {
    @Override
    public String handleRequest(Object input, DurableContext context) {
        // Java requires a name — use a descriptive string
        String result = context.step("my_step", String.class,
            (StepContext ctx) -> "some value");
        return result;
    }
}

Pass "" as the name for an unnamed inline step.

package main

import "github.com/aws/aws-durable-execution-sdk-go/durable"

func handler(ctx durable.Context, event map[string]any) (string, error) {
    // Pass "" as the name for an unnamed step.
    return durable.Step(ctx, "", func(sc durable.StepContext) (string, error) {
        return "some value", nil
    })
}

func main() {
    durable.Start(handler)
}
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;

public class LambdaStepNoNameExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<object, string>(Workflow, input, context);

    private async Task<string> Workflow(object input, IDurableContext ctx)
    {
        // Omit name — the SDK infers one from the call site
        string result = await ctx.StepAsync(async (_, _) => "some value");
        return result;
    }
}

Pass arguments to the step function

Capture arguments in a closure:

import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js";

async function myStep(arg1: string, arg2: number): Promise<string> {
  return `${arg1}: ${arg2}`;
}

export const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    const result = await context.step("my_step", async () => myStep("value", 42));
    return result;
  },
);

Use @durable_step and pass arguments when calling the function:

@durable_step
def my_step(step_context: StepContext, arg1: str, arg2: int) -> str:
    return f"{arg1}: {arg2}"

result = context.step(my_step("value", 42))

Capture arguments in a lambda:

import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.StepContext;

public class MultiArgumentStepExample extends DurableHandler<Object, String> {
    @Override
    public String handleRequest(Object input, DurableContext context) {
        String arg1 = "value";
        int arg2 = 42;

        String result = context.step("my_step", String.class,
            (StepContext ctx) -> arg1 + ": " + arg2);

        return result;
    }
}

Capture arguments in the closure. The step body takes only a durable.StepContext.

package main

import (
    "fmt"

    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

func myStep(arg1 string, arg2 int) (string, error) {
    return fmt.Sprintf("%s: %d", arg1, arg2), nil
}

func handler(ctx durable.Context, event map[string]any) (string, error) {
    // Capture arguments in the closure. The step body takes only StepContext.
    arg1, arg2 := "value", 42
    return durable.Step(ctx, "my_step", func(sc durable.StepContext) (string, error) {
        return myStep(arg1, arg2)
    })
}

func main() {
    durable.Start(handler)
}

Capture arguments in the closure:

using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;

public class MultiArgumentStepExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<object, string>(Workflow, input, context);

    private async Task<string> Workflow(object input, IDurableContext ctx)
    {
        string arg1 = "value";
        int arg2 = 42;

        // Capture arguments in the closure
        string result = await ctx.StepAsync(
            async (_, _) => MyStep(arg1, arg2),
            name: "my_step");
        return result;
    }

    private static string MyStep(string arg1, int arg2) => $"{arg1}: {arg2}";
}

Naming steps

Name your steps so they're easy to identify in logs and tests. Use descriptive names that explain what the step does. Names don't need to be unique, but distinct names make debugging easier.

The name is the first argument. Pass undefined to omit it.

The @durable_step decorator uses the function's name automatically as the step name. Override it with the name keyword argument.

The name is always the first argument. Pass null for no name.

The name is the second argument, after the context. Pass "" to omit it.

The name is the optional name argument. Omit it to infer one from the call site.

Configuration

Configure step behavior using StepConfig:

import {
  DurableContext,
  StepConfig,
  StepSemantics,
  createRetryStrategy,
  withDurableExecution,
} from "@aws/durable-execution-sdk-js";

async function processData(data: string): Promise<object> {
  return { processed: data, status: "completed" };
}

const stepConfig: StepConfig<object> = {
  retryStrategy: createRetryStrategy({
    maxAttempts: 3,
  }),
  semantics: StepSemantics.AtLeastOncePerRetry,
};

export const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    const result = await context.step(
      "process_data",
      async () => processData(event.data),
      stepConfig,
    );
    return result;
  },
);
from aws_durable_execution_sdk_python import (
    DurableContext,
    StepContext,
    durable_execution,
    durable_step,
)
from aws_durable_execution_sdk_python.config import StepConfig, StepSemantics
from aws_durable_execution_sdk_python.retries import (
    RetryStrategyConfig,
    create_retry_strategy,
)


@durable_step
def process_data(step_context: StepContext, data: str) -> dict:
    return {"processed": data, "status": "completed"}


@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
    retry_config = RetryStrategyConfig(
        max_attempts=3,
        retryable_error_types=[RuntimeError, ValueError],
    )

    step_config = StepConfig(
        retry_strategy=create_retry_strategy(retry_config),
        step_semantics=StepSemantics.AT_LEAST_ONCE_PER_RETRY,
    )

    result = context.step(process_data(event["data"]), config=step_config)
    return result
import java.util.Map;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.StepContext;
import software.amazon.lambda.durable.config.StepConfig;
import software.amazon.lambda.durable.config.StepSemantics;
import software.amazon.lambda.durable.retry.RetryStrategies;

public class ProcessDataExample extends DurableHandler<Map<String, Object>, Map<String, Object>> {
    @Override
    public Map<String, Object> handleRequest(Map<String, Object> event, DurableContext context) {
        StepConfig config = StepConfig.builder()
            .retryStrategy(RetryStrategies.exponentialBackoff(
                3,
                java.time.Duration.ofSeconds(1),
                java.time.Duration.ofMinutes(5),
                2.0,
                software.amazon.lambda.durable.retry.JitterStrategy.FULL))
            .semanticsPerRetry(StepSemantics.AT_LEAST_ONCE_PER_RETRY)
            .build();

        Map<String, Object> result = context.step("process_data", Map.class,
            (StepContext ctx) -> Map.of("processed", event.get("data"), "status", "completed"),
            config);

        return result;
    }
}
package main

import "github.com/aws/aws-durable-execution-sdk-go/durable"

type DataEvent struct {
    Data string `json:"data"`
}

type Processed struct {
    Processed string `json:"processed"`
    Status    string `json:"status"`
}

func processData(data string) (Processed, error) {
    return Processed{Processed: data, Status: "completed"}, nil
}

func handler(ctx durable.Context, event DataEvent) (Processed, error) {
    retry := durable.MustNewRetryStrategy(durable.RetryConfig{MaxAttempts: 3})
    return durable.Step(ctx, "process_data",
        func(sc durable.StepContext) (Processed, error) {
            return processData(event.Data)
        },
        durable.WithRetry(retry),
        durable.WithSemantics(durable.AtLeastOncePerRetry),
    )
}

func main() {
    durable.Start(handler)
}
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;

public class ProcessDataExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<DataEvent, Processed>(Workflow, input, context);

    private async Task<Processed> Workflow(DataEvent input, IDurableContext ctx)
    {
        var config = new StepConfig
        {
            RetryStrategy = RetryStrategy.Exponential(maxAttempts: 3),
            Semantics = StepSemantics.AtLeastOncePerRetry,
        };

        Processed result = await ctx.StepAsync(
            async (_, _) => ProcessData(input.Data),
            name: "process_data",
            config: config);
        return result;
    }

    private static Processed ProcessData(string data) => new(data, Status: "completed");
}

public record DataEvent(string Data);
public record Processed(string Data, string Status);

Pass data between steps

Pass data between steps through return values. Do not use shared variables or closure mutations. Steps return cached results on replay, so mutations to outer variables are lost.

wrong way to pass data between steps

import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js";

async function registerUser(email: string): Promise<string> {
  return `user-${email}`;
}

async function sendFollowUpEmail(userId: string): Promise<void> {
  // send email to user
}

// ❌ WRONG: userId mutation is lost on replay after the wait
export const handler = withDurableExecution(
  async (event: { email: string }, context: DurableContext) => {
    let userId = "";

    await context.step("register-user", async () => {
      userId = await registerUser(event.email); // ⚠️ Lost on replay!
    });

    await context.wait("follow-up-delay", { minutes: 10 });

    await context.step("send-follow-up-email", async () => {
      await sendFollowUpEmail(userId); // userId is "" on replay
    });
  },
);
from aws_durable_execution_sdk_python import (
    DurableContext,
    StepContext,
    durable_execution,
    durable_step,
)
from aws_durable_execution_sdk_python.config import Duration


def register_user(email: str) -> str:
    return f"user-{email}"


def send_follow_up_email(user_id: str) -> None:
    # send email to user
    pass


# ❌ WRONG: user_id mutation is lost on replay after the wait
@durable_execution
def handler(event: dict, context: DurableContext) -> None:
    user_id = ""

    @durable_step
    def do_register(step_context: StepContext) -> None:
        nonlocal user_id
        user_id = register_user(event["email"])  # ⚠️ Lost on replay!

    context.step(do_register())

    context.wait(Duration.from_minutes(10), name="follow-up-delay")

    @durable_step
    def do_send(step_context: StepContext) -> None:
        send_follow_up_email(user_id)  # user_id is "" on replay

    context.step(do_send())
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;

public class PassingDataWrongExample extends DurableHandler<Map<String, String>, Void> {

    private String registerUser(String email) {
        return "user-" + email;
    }

    private void sendFollowUpEmail(String userId) {
        // send email to user
    }

    @Override
    public Void handleRequest(Map<String, String> event, DurableContext context) {
        // ❌ WRONG: userId mutation is lost on replay after the wait
        AtomicReference<String> userId = new AtomicReference<>("");

        context.step("register-user", String.class, ctx -> {
            userId.set(registerUser(event.get("email"))); // ⚠️ Lost on replay!
            return userId.get();
        });

        context.wait("follow-up-delay", Duration.ofMinutes(10));

        context.step("send-follow-up-email", Void.class, ctx -> {
            sendFollowUpEmail(userId.get()); // userId is "" on replay
            return null;
        });

        return null;
    }
}
package main

import (
    "time"

    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

type EmailEvent struct {
    Email string `json:"email"`
}

func registerUser(email string) (string, error) {
    return "user-" + email, nil
}

func sendFollowUpEmail(userID string) error {
    // send email to user
    return nil
}

// WRONG: the mutation of userID is lost on replay after the wait.
func handler(ctx durable.Context, event EmailEvent) (durable.Void, error) {
    userID := ""

    if _, err := durable.Step(ctx, "register-user", func(sc durable.StepContext) (durable.Void, error) {
        id, err := registerUser(event.Email)
        userID = id // lost on replay
        return durable.Void{}, err
    }); err != nil {
        return durable.Void{}, err
    }

    if err := durable.Wait(ctx, "follow-up-delay", 10*time.Minute); err != nil {
        return durable.Void{}, err
    }

    return durable.Step(ctx, "send-follow-up-email", func(sc durable.StepContext) (durable.Void, error) {
        return durable.Void{}, sendFollowUpEmail(userID) // userID is "" on replay
    })
}

func main() {
    durable.Start(handler)
}
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;

public class PassingDataWrongExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<UserEvent>(Workflow, input, context);

    // WRONG: userId mutation is lost on replay after the wait
    private async Task Workflow(UserEvent input, IDurableContext ctx)
    {
        string userId = "";

        await ctx.StepAsync(
            async (_, _) => { userId = RegisterUser(input.Email); }, // Lost on replay!
            name: "register-user");

        await ctx.WaitAsync(TimeSpan.FromMinutes(10), name: "follow-up-delay");

        await ctx.StepAsync(
            async (_, _) => SendFollowUpEmail(userId), // userId is "" on replay
            name: "send-follow-up-email");
    }

    private static string RegisterUser(string email) => $"user-{email}";

    private static void SendFollowUpEmail(string userId)
    {
        // send email to user
    }
}

public record UserEvent(string Email);

correct way to pass data between steps

import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js";

async function registerUser(email: string): Promise<string> {
  return `user-${email}`;
}

async function sendFollowUpEmail(userId: string): Promise<void> {
  // send email to user
}

// ✅ CORRECT: userId is returned from the step and restored from checkpoint on replay
export const handler = withDurableExecution(
  async (event: { email: string }, context: DurableContext) => {
    const userId = await context.step("register-user", async () => {
      return await registerUser(event.email);
    });

    await context.wait("follow-up-delay", { minutes: 10 });

    await context.step("send-follow-up-email", async () => {
      await sendFollowUpEmail(userId); // userId restored from checkpoint
    });
  },
);
from aws_durable_execution_sdk_python import (
    DurableContext,
    StepContext,
    durable_execution,
    durable_step,
)
from aws_durable_execution_sdk_python.config import Duration


def register_user(email: str) -> str:
    return f"user-{email}"


def send_follow_up_email(user_id: str) -> None:
    # send email to user
    pass


# ✅ CORRECT: user_id is returned from the step and restored from checkpoint on replay
@durable_execution
def handler(event: dict, context: DurableContext) -> None:
    @durable_step
    def do_register(step_context: StepContext) -> str:
        return register_user(event["email"])

    user_id = context.step(do_register())

    context.wait(Duration.from_minutes(10), name="follow-up-delay")

    @durable_step
    def do_send(step_context: StepContext) -> None:
        send_follow_up_email(user_id)  # user_id restored from checkpoint

    context.step(do_send())
import java.time.Duration;
import java.util.Map;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;

public class PassingDataCorrectExample extends DurableHandler<Map<String, String>, Void> {

    private String registerUser(String email) {
        return "user-" + email;
    }

    private void sendFollowUpEmail(String userId) {
        // send email to user
    }

    @Override
    public Void handleRequest(Map<String, String> event, DurableContext context) {
        // ✅ CORRECT: userId is returned from the step and restored from checkpoint on replay
        String userId = context.step("register-user", String.class,
            ctx -> registerUser(event.get("email")));

        context.wait("follow-up-delay", Duration.ofMinutes(10));

        context.step("send-follow-up-email", Void.class, ctx -> {
            sendFollowUpEmail(userId); // userId restored from checkpoint
            return null;
        });

        return null;
    }
}
package main

import (
    "time"

    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

type EmailEvent struct {
    Email string `json:"email"`
}

func registerUser(email string) (string, error) {
    return "user-" + email, nil
}

func sendFollowUpEmail(userID string) error {
    // send email to user
    return nil
}

// CORRECT: userID is returned from the step and restored from the checkpoint on replay.
func handler(ctx durable.Context, event EmailEvent) (durable.Void, error) {
    userID, err := durable.Step(ctx, "register-user", func(sc durable.StepContext) (string, error) {
        return registerUser(event.Email)
    })
    if err != nil {
        return durable.Void{}, err
    }

    if err := durable.Wait(ctx, "follow-up-delay", 10*time.Minute); err != nil {
        return durable.Void{}, err
    }

    return durable.Step(ctx, "send-follow-up-email", func(sc durable.StepContext) (durable.Void, error) {
        return durable.Void{}, sendFollowUpEmail(userID) // userID restored from the checkpoint
    })
}

func main() {
    durable.Start(handler)
}
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;

public class PassingDataCorrectExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<UserEvent>(Workflow, input, context);

    // CORRECT: userId is returned from the step and restored from checkpoint on replay
    private async Task Workflow(UserEvent input, IDurableContext ctx)
    {
        string userId = await ctx.StepAsync(
            async (_, _) => RegisterUser(input.Email),
            name: "register-user");

        await ctx.WaitAsync(TimeSpan.FromMinutes(10), name: "follow-up-delay");

        await ctx.StepAsync(
            async (_, _) => SendFollowUpEmail(userId), // userId restored from checkpoint
            name: "send-follow-up-email");
    }

    private static string RegisterUser(string email) => $"user-{email}";

    private static void SendFollowUpEmail(string userId)
    {
        // send email to user
    }
}

public record UserEvent(string Email);

Nesting steps

You cannot nest steps. Do not attempt to invoke another step from inside a step. If you want to group or nest operations, use a child context.

Concurrency

Do not run steps concurrently. For concurrent operations, see map and parallel.

To code your own concurrency use a child context to encapsulate each concurrent code path.

See also

Checkpoint consumption

Durable operations consume checkpoints. To understand how this operation affects your checkpoint usage, see Checkpoint consumption.