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)
}
use aws_durable_execution_sdk as durable;

fn add_numbers(_ctx: durable::StepContext, a: i32, b: i32) -> i32 {
    a + b
}

async fn handler(
    _event: serde_json::Value,
    ctx: durable::DurableContext,
) -> Result<i32, durable::BoxError> {
    let result = ctx
        .step(|step_ctx| async move { Ok(add_numbers(step_ctx, 5, 3)) })
        .name("add_numbers")
        .await?;
    Ok(result)
}

#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    durable::init_default_subscriber()?;
    durable::run(handler).await
}
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.

impl DurableContext {
    // ...
    pub fn step<O, F, Fut>(&self, f: F) -> StepBuilder<O, F, Fut>
    where
        F: FnOnce(StepContext) -> Fut + Clone + Send + 'static,
        Fut: Future<Output = Result<O, BoxError>> + Send + 'static,
        O: Send + 'static;
    // ...
}

impl<O, F, Fut, S> StepBuilder<O, F, Fut, S>
// ...
{
    pub fn future(self) -> DurableFuture<O>;
    // ...
    pub fn spawn(self) -> DurableFuture<O>;
    // ...
}

impl<O, F, Fut, S> IntoFuture for StepBuilder<O, F, Fut, S>
// ...
{
    type Output = Result<O, OperationError>;
    type IntoFuture = DurableFuture<O>;
    // ...
}

Parameters:

  • f (required) A closure that receives a durable::StepContext and returns a future of Result<O, durable::BoxError>. The closure must be Clone + Send + 'static. Captured local values must be owned. The SDK calls a fresh clone of the closure for each attempt.
  • .name(...) (optional) A name for the step. Omit the call to leave the step unnamed.
  • The configuration methods in StepConfig (optional).

Returns: a StepBuilder. Use .await to run the step and get Result<O, durable::OperationError>. .spawn() starts the step now and returns a durable::DurableFuture<O> that resolves to the same Result. .future() returns a DurableFuture<O> without starting the step, as input for ctx.join_all, ctx.try_join_all, ctx.race, and ctx.select_ok.

Errors: durable::OperationError. Match err.kind() against durable::OperationErrorKind. A failed step gives OperationErrorKind::Step, whose kind() is a durable::StepErrorKind:

  • ExecutionFailed when the body returns Err and the retry strategy stops.
  • SerializationFailed when the step's serdes cannot serialize or deserialize the result. The step then fails at once, without a retry.
  • Interrupted when an AtMostOncePerRetry attempt was interrupted before the SDK recorded its outcome, and the retry strategy stops. The recorded error type is "StepInterruptedError".

For a recorded body failure or interrupted attempt, err.wire() returns the recorded error type and message on the first run and on replay. Use err.kind() to classify every step failure because wire() is optional. A live source() may contain the body's own error. Replay replaces it with a recorded failure. A step whose name or subtype differs from its record on replay fails with OperationErrorKind::NonDeterministicExecution.

// 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.

Configure a step with methods on its StepBuilder before .await.

impl<O, F, Fut, S> StepBuilder<O, F, Fut, S>
where
    O: Send + 'static,
    F: FnOnce(StepContext) -> Fut + Clone + Send + 'static,
    Fut: Future<Output = Result<O, BoxError>> + Send + 'static,
{
    pub fn name(mut self, name: impl Into<String>) -> Self;
    // ...
    pub fn retry_strategy<R>(mut self, strategy: R) -> Self
    where
        R: Fn(&crate::StepError, u32) -> crate::RetryDecision + Send + Sync + 'static;
    // ...
    pub fn retry_strategy_config(mut self, config: crate::builders::RetryStrategyConfig) -> Self;
    // ...
    pub fn serdes<S2>(self, serdes: S2) -> StepBuilder<O, F, Fut, S2>
    where
        S2: Serdes<O>;
    // ...
    pub fn semantics(mut self, semantics: crate::step::StepSemantics) -> Self;
    // ...
    pub fn sub_type(mut self, sub_type: OperationSubType) -> Self;
    // ...
}

Parameters:

  • retry_strategy (optional) A closure that receives the durable::StepError and the number of the attempt that just failed, starting at 1, and returns a durable::RetryDecision. RetryDecision::retry(delay) retries after the delay, rounded up to whole seconds with a minimum of one second. RetryDecision::Stop fails the step. See Retry strategies.
  • retry_strategy_config (optional) The same decision from a durable::builders::RetryStrategyConfig. When you set neither retry method, the step uses RetryStrategyConfig::default_preset(). 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 its strategy stops it. A second call of either retry method replaces the first.
  • semantics (optional) durable::StepSemantics::AtLeastOncePerRetry (default) or StepSemantics::AtMostOncePerRetry.
  • serdes (optional) A custom durable::Serdes<O> for the step result. The default is durable::JsonSerdes, which needs the result type to implement Serialize and DeserializeOwned. See Serialization.
  • sub_type (optional) The operation subtype the step records in the checkpoint. The default is Step. durable::OperationSubType::new accepts 1 to 32 characters from A-Z, a-z, 0-9, hyphen, and underscore, and returns an InvalidOperationSubType error for any other value. The subtype is part of the step's replay identity, so a different subtype on replay fails with OperationErrorKind::NonDeterministicExecution.
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.

pub struct StepContext {
    // ...
}

impl StepContext {
    // ...
    pub fn attempt(&self) -> u32;
    // ...
}
  • attempt() The current attempt number, starting at 1 for the first attempt.

StepContext exposes no durable operations. Log with the macros of the tracing crate. A record inside a step body carries the step's operationId, operationName, and attempt. See Logging.

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.
pub enum StepSemantics {
    AtLeastOncePerRetry,
    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::StepError of kind StepErrorKind::Interrupted 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, the step fails with that error, and the recorded error type is "StepInterruptedError". Use for operations with side effects.

The enum is #[non_exhaustive], so a match on it needs a _ arm.

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)
}

A step body is a closure that takes a durable::StepContext and returns a future. Call your own function from it, and .await the builder for the result. Use .spawn() to start the step and get a durable::DurableFuture<O>.

use aws_durable_execution_sdk as durable;
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct OrderEvent {
    order_id: String,
}

#[derive(Serialize, Deserialize)]
struct Validation {
    order_id: String,
    valid: bool,
}

fn validate_order(_ctx: durable::StepContext, order_id: String) -> Validation {
    Validation {
        order_id,
        valid: true,
    }
}

async fn handler(
    event: OrderEvent,
    ctx: durable::DurableContext,
) -> Result<Validation, durable::BoxError> {
    let order_id = event.order_id;
    let validation = ctx
        .step(move |step_ctx| async move { Ok(validate_order(step_ctx, order_id)) })
        .name("validate_order")
        .await?;
    Ok(validation)
}

#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    durable::init_default_subscriber()?;
    durable::run(handler).await
}

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)
}

Omit .name(...) for an unnamed step.

use aws_durable_execution_sdk as durable;

async fn handler(
    _event: serde_json::Value,
    ctx: durable::DurableContext,
) -> Result<String, durable::BoxError> {
    // The step stays unnamed.
    let result = ctx.step(|_| async { Ok("some value".to_owned()) }).await?;
    Ok(result)
}

#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    durable::init_default_subscriber()?;
    durable::run(handler).await
}
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. The closure must be Clone + Send + 'static. Captured local values must be owned, and each attempt calls a fresh clone of the closure.

use aws_durable_execution_sdk as durable;

fn my_step(arg1: &str, arg2: i32) -> String {
    format!("{arg1}: {arg2}")
}

async fn handler(
    _event: serde_json::Value,
    ctx: durable::DurableContext,
) -> Result<String, durable::BoxError> {
    let result = ctx
        .step(|_| async { Ok(my_step("value", 42)) })
        .name("my_step")
        .await?;
    Ok(result)
}

#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    durable::init_default_subscriber()?;
    durable::run(handler).await
}

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.

Set the name with .name("...") on the builder. Omit the call to leave the step unnamed.

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)
}
use aws_durable_execution_sdk as durable;
use durable::StepSemantics;
use durable::builders::RetryStrategyConfig;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Processed {
    processed: serde_json::Value,
    status: String,
}

fn process_data(data: serde_json::Value) -> Processed {
    Processed {
        processed: data,
        status: "completed".to_owned(),
    }
}

async fn handler(
    event: serde_json::Value,
    ctx: durable::DurableContext,
) -> Result<Processed, durable::BoxError> {
    let data = event.get("data").cloned().unwrap_or_default();
    let result = ctx
        .step(move |_| async move { Ok(process_data(data)) })
        .name("process_data")
        .retry_strategy_config(RetryStrategyConfig::builder().max_attempts(3).build())
        .semantics(StepSemantics::AtLeastOncePerRetry)
        .await?;
    Ok(result)
}

#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    durable::init_default_subscriber()?;
    durable::run(handler).await
}
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)
}

A step closure is 'static and cannot borrow a local variable, so this example shares the variable through an Arc<Mutex<String>>. The value the step sets is still lost on replay.

use std::sync::{Arc, Mutex};
use std::time::Duration;

use aws_durable_execution_sdk as durable;
use serde::Deserialize;

#[derive(Deserialize)]
struct SignupEvent {
    email: String,
}

fn register_user(email: &str) -> String {
    format!("user-{email}")
}

fn send_follow_up_email(_user_id: &str) {}

// ❌ WRONG: the user_id mutation is lost on replay after the wait
async fn handler(
    event: SignupEvent,
    ctx: durable::DurableContext,
) -> Result<(), durable::BoxError> {
    let user_id = Arc::new(Mutex::new(String::new()));

    let shared = Arc::clone(&user_id);
    ctx.step(move |_| async move {
        let registered = register_user(&event.email);
        *shared.lock().map_err(|e| e.to_string())? = registered;
        Ok(())
    })
    .name("register-user")
    .await?;

    ctx.wait(Duration::from_mins(10))
        .name("follow-up-delay")
        .await?;

    let shared = Arc::clone(&user_id);
    ctx.step(move |_| async move {
        let user_id = shared.lock().map_err(|e| e.to_string())?.clone();
        send_follow_up_email(&user_id); // user_id is "" on replay
        Ok(())
    })
    .name("send-follow-up-email")
    .await?;

    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    durable::init_default_subscriber()?;
    durable::run(handler).await
}
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)
}
use std::time::Duration;

use aws_durable_execution_sdk as durable;
use serde::Deserialize;

#[derive(Deserialize)]
struct SignupEvent {
    email: String,
}

fn register_user(email: &str) -> String {
    format!("user-{email}")
}

fn send_follow_up_email(_user_id: &str) {}

// ✅ CORRECT: user_id is returned from the step and restored from checkpoint on replay
async fn handler(
    event: SignupEvent,
    ctx: durable::DurableContext,
) -> Result<(), durable::BoxError> {
    let user_id: String = ctx
        .step(move |_| async move { Ok(register_user(&event.email)) })
        .name("register-user")
        .await?;

    ctx.wait(Duration::from_mins(10))
        .name("follow-up-delay")
        .await?;

    ctx.step(move |_| async move {
        send_follow_up_email(&user_id);
        Ok(())
    })
    .name("send-follow-up-email")
    .await?;

    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    durable::init_default_subscriber()?;
    durable::run(handler).await
}
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.