Skip to content

Child Context

Isolate execution scope

A child context creates an isolated execution scope within a durable function for grouping operations. It has its own operation namespace and its own set of checkpoints. Unlike a step, which wraps a single function call, a child context can contain multiple durable operations, such as steps, waits, and other operations.

When the child context completes, the SDK checkpoints the result as a single unit in the parent context. On replay, the SDK returns the checkpointed result without re-running the operations inside the child context. If the result exceeds the checkpoint size limit the child context will reconstruct the result in memory from the checkpointed results of its child operations without rerunning the child operations.

Use child contexts to group multiple durable operations. This is useful to organize complex workflows, implement sub-workflows and maintain determinism when running multiple child contexts concurrently.

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

export const handler = withDurableExecution(async (event: any, context: DurableContext) => {
  const result = await context.runInChildContext("process-order", async (child) => {
    const validated = await child.step("validate", async () => validate(event.orderId));
    const charged = await child.step("charge", async () => charge(validated));
    return charged;
  });
  return result;
});

async function validate(orderId: string) { return { orderId, valid: true }; }
async function charge(order: { orderId: string; valid: boolean }) { return { ...order, charged: true }; }
from aws_durable_execution_sdk_python import (
    DurableContext,
    StepContext,
    durable_execution,
    durable_step,
    durable_with_child_context,
)


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


@durable_step
def charge(ctx: StepContext, order: dict) -> dict:
    return {**order, "charged": True}


@durable_with_child_context
def process_order(ctx: DurableContext, order_id: str) -> dict:
    validated = ctx.step(validate(order_id))
    return ctx.step(charge(validated))


@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
    return context.run_in_child_context(process_order(event["order_id"]))
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;

public class BasicChildContext extends DurableHandler<String, String> {

    @Override
    public String handleRequest(String orderId, DurableContext context) {
        return context.runInChildContext("process-order", String.class, child -> {
            String validated = child.step("validate", String.class, ctx -> validate(orderId));
            return child.step("charge", String.class, ctx -> charge(validated));
        });
    }

    private String validate(String orderId) { return orderId + ":validated"; }
    private String charge(String order) { return order + ":charged"; }
}

Method signature

Run in ChildContext

// Named overload
runInChildContext<TOutput>(
  name: string | undefined,
  fn: (context: DurableContext) => Promise<TOutput>,
  config?: ChildConfig<TOutput>,
): DurablePromise<TOutput>;

// Unnamed overload
runInChildContext<TOutput>(
  fn: (context: DurableContext) => Promise<TOutput>,
  config?: ChildConfig<TOutput>,
): DurablePromise<TOutput>;

Parameters:

  • name (optional) A name for the child context. Pass undefined to omit.
  • fn An async function that receives a DurableContext and returns Promise<T>.
  • config (optional) A ChildConfig<T> object.

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

Throws: ChildContextError wrapping the original error if the child context function throws.

def run_in_child_context(
    self,
    func: Callable[[DurableContext], T],
    name: str | None = None,
    config: ChildConfig | None = None,
) -> T: ...

Parameters:

  • func A callable that receives a DurableContext and returns T. Use the @durable_with_child_context decorator to create one.
  • name (optional) A name for the child context. Defaults to the function's name when using @durable_with_child_context.
  • config (optional) A ChildConfig object.

Returns: T, the return value of func.

Raises: CallableRuntimeError wrapping the original exception if the child context function raises.

// sync
<T> T runInChildContext(String name, Class<T> resultType, Function<DurableContext, T> func)
<T> T runInChildContext(String name, Class<T> resultType, Function<DurableContext, T> func, RunInChildContextConfig config)
<T> T runInChildContext(String name, TypeToken<T> resultType, Function<DurableContext, T> func)
<T> T runInChildContext(String name, TypeToken<T> resultType, Function<DurableContext, T> func, RunInChildContextConfig config)

// async
<T> DurableFuture<T> runInChildContextAsync(String name, Class<T> resultType, Function<DurableContext, T> func)
<T> DurableFuture<T> runInChildContextAsync(String name, Class<T> resultType, Function<DurableContext, T> func, RunInChildContextConfig config)
<T> DurableFuture<T> runInChildContextAsync(String name, TypeToken<T> resultType, Function<DurableContext, T> func)
<T> DurableFuture<T> runInChildContextAsync(String name, TypeToken<T> resultType, Function<DurableContext, T> func, RunInChildContextConfig config)

The name is always required. Pass null to omit it.

Parameters:

  • name (required) A name for the child context. Pass null to omit.
  • resultType The Class<T> or TypeToken<T> for deserialization.
  • func A Function<DurableContext, T> to execute.
  • config (optional) A RunInChildContextConfig object.

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

Throws: The original exception re-thrown after deserialization if possible, otherwise ChildContextFailedException.

Child Config

interface ChildConfig<T> {
  serdes?: Serdes<T>;
  subType?: string;
  summaryGenerator?: (result: T) => string;
  errorMapper?: (originalError: DurableOperationError) => DurableOperationError;
  virtualContext?: boolean;
}

Parameters:

  • serdes (optional) Custom Serdes<T> for the child context result. See Serialization.
  • subType (optional) An internal subtype identifier. Used by map and parallel internally; not needed for direct use.
  • summaryGenerator (optional) A function that generates a compact summary when the result exceeds the checkpoint size limit. Used internally by map and parallel.
  • errorMapper (optional) A function that maps child context errors to custom error types.
  • virtualContext (optional) If true, skips checkpointing and uses the parent's ID for child operations.
@dataclass(frozen=True)
class ChildConfig(Generic[T]):
    serdes: SerDes | None = None
    item_serdes: SerDes | None = None
    sub_type: OperationSubType | None = None
    summary_generator: SummaryGenerator | None = None

Parameters:

  • serdes (optional) Custom SerDes for the child context result. See Serialization.
  • item_serdes (optional) Custom SerDes for individual items within the child context.
  • sub_type (optional) An internal subtype identifier. Used by map and parallel internally; not needed for direct use.
  • summary_generator (optional) A function that generates a compact summary when the result exceeds the checkpoint size limit. Used internally by map and parallel.
RunInChildContextConfig.builder()
    .serDes(SerDes)  // optional
    .build()

Parameters:

  • serDes (optional) Custom SerDes for the child context result. See Serialization.

The child context's function

The child context function receives a DurableContext as its argument. This is the child context.

Code inside the child context can can call any durable operation on that child context, such as steps, waits, callbacks and further nested child contexts.

Do not use the parent context inside the child context via closure because it will corrupt execution state and cause non-deterministic behaviour.

Pass any async function directly. The function receives a DurableContext and must return a Promise<T>.

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

export const handler = withDurableExecution(async (event: any, context: DurableContext) => {
  // Pass an inline async function directly
  const result = await context.runInChildContext("process-order", async (child) => {
    const step1 = await child.step("validate", async () => validate(event.orderId));
    const step2 = await child.step("charge", async () => charge(step1));
    return step2;
  });
  return result;
});

async function validate(orderId: string) { return { orderId, valid: true }; }
async function charge(order: { orderId: string; valid: boolean }) { return { ...order, charged: true }; }

Use the @durable_with_child_context decorator. It wraps your function so it can be called with arguments and passed to context.run_in_child_context(). The decorator automatically uses the function's name as the child context name.

from aws_durable_execution_sdk_python import (
    DurableContext,
    StepContext,
    durable_execution,
    durable_step,
    durable_with_child_context,
)


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


@durable_step
def charge(ctx: StepContext, order: dict) -> dict:
    return {**order, "charged": True}


# The decorator wraps the function so it can be called with arguments
# and passed to context.run_in_child_context().
@durable_with_child_context
def process_order(ctx: DurableContext, order_id: str) -> dict:
    validated = ctx.step(validate(order_id))
    return ctx.step(charge(validated))


@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
    return context.run_in_child_context(process_order(event["order_id"]))

Pass a lambda or method reference directly. The function receives a DurableContext and returns T.

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

public class ContextFunction extends DurableHandler<String, String> {

    @Override
    public String handleRequest(String orderId, DurableContext context) {
        // Pass a lambda directly as the context function
        return context.runInChildContext("process-order", String.class, child -> {
            String validated = child.step("validate", String.class, ctx -> validate(orderId));
            return child.step("charge", String.class, ctx -> charge(validated));
        });
    }

    private String validate(String orderId) { return orderId + ":validated"; }
    private String charge(String order) { return order + ":charged"; }
}

Pass arguments to the child context

Capture arguments in a closure:

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

export const handler = withDurableExecution(async (event: any, context: DurableContext) => {
  const { orderId, userId } = event;

  // Capture arguments in a closure
  const result = await context.runInChildContext("process-order", async (child) => {
    const validated = await child.step("validate", async () => validate(orderId, userId));
    return await child.step("charge", async () => charge(validated));
  });

  return result;
});

async function validate(orderId: string, userId: string) { return { orderId, userId }; }
async function charge(order: { orderId: string; userId: string }) { return { ...order, charged: true }; }

Pass arguments when calling the decorated function:

from aws_durable_execution_sdk_python import (
    DurableContext,
    StepContext,
    durable_execution,
    durable_step,
    durable_with_child_context,
)


@durable_step
def validate(ctx: StepContext, order_id: str, user_id: str) -> dict:
    return {"order_id": order_id, "user_id": user_id}


@durable_step
def charge(ctx: StepContext, order: dict) -> dict:
    return {**order, "charged": True}


# Pass arguments when calling the decorated function
@durable_with_child_context
def process_order(ctx: DurableContext, order_id: str, user_id: str) -> dict:
    validated = ctx.step(validate(order_id, user_id))
    return ctx.step(charge(validated))


@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
    return context.run_in_child_context(
        process_order(event["order_id"], event["user_id"])
    )

Capture arguments in a lambda:

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

public class PassArguments extends DurableHandler<String, String> {

    @Override
    public String handleRequest(String orderId, DurableContext context) {
        String userId = "user-123";

        // Capture arguments in a lambda
        return context.runInChildContext("process-order", String.class, child -> {
            String validated = child.step("validate", String.class, ctx -> validate(orderId, userId));
            return child.step("charge", String.class, ctx -> charge(validated));
        });
    }

    private String validate(String orderId, String userId) { return orderId + ":" + userId; }
    private String charge(String order) { return order + ":charged"; }
}

Naming child contexts

Name child contexts to make them easier to identify in logs and tests.

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

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

export const handler = withDurableExecution(async (event: any, context: DurableContext) => {
  // Pass undefined to omit the name
  const unnamed = await context.runInChildContext(async (child) => {
    return await child.step(async () => "result");
  });

  // Pass a name as the first argument
  const named = await context.runInChildContext("process-order", async (child) => {
    return await child.step(async () => "result");
  });

  return { unnamed, named };
});

The @durable_with_child_context decorator uses the function's name automatically. Override it with the name keyword argument to run_in_child_context().

from aws_durable_execution_sdk_python import (
    DurableContext,
    durable_execution,
    durable_with_child_context,
)


@durable_with_child_context
def process_order(ctx: DurableContext, order_id: str) -> str:
    return ctx.step(lambda _: order_id + ":processed", name="process")


@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
    # The name defaults to the function's name when using @durable_with_child_context
    auto_named = context.run_in_child_context(process_order(event["order_id"]))

    # Override the name explicitly
    explicit = context.run_in_child_context(
        process_order(event["order_id"]),
        name="process-order",
    )

    return {"auto_named": auto_named, "explicit": explicit}

The name is always the first argument. Pass null to omit it.

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

public class NamedChildContext extends DurableHandler<String, String> {

    @Override
    public String handleRequest(String orderId, DurableContext context) {
        // The name is always required. Pass null to omit it.
        String unnamed = context.runInChildContext(null, String.class, child ->
            child.step("process", String.class, ctx -> orderId + ":processed")
        );

        String named = context.runInChildContext("process-order", String.class, child ->
            child.step("process", String.class, ctx -> orderId + ":processed")
        );

        return unnamed + " | " + named;
    }
}

Concurrency

Note

Parallel and map operations manage the complexity of concurrency for you with concurrency control and completion policies, so you don't have to code it yourself using child contexts.

It is not deterministic to run durable operations concurrently without wrapping each concurrent branch in its own child context. The reason for this is that to ensure deterministic replay, each durable operation gets an incrementing ID from a sequential counter. If two operations start concurrently, the counter increments in whatever order they happen to execute. On replay that order could differ, which could result in unexpected behaviour. For example, an operation could receive a different operation ID on replay and then retrieve a different operation's result.

A child context has its own isolated operation ID counter, so internal operations do not affect the parent's checkpoint state. You must still start each child context sequentially in the parent.

Concurrency rules

  1. All durable operations inside a context must start sequentially.
  2. To run durable operations concurrently, enclose each set of operations in its own child context.
  3. Start each child context serially. You do not have to wait for the previous child context to complete before starting the next.
  4. Inside the child function you must use the child context argument, not the parent context.

Don't await each child context immediately. Start them all, then await together.

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

// WRONG - a and b share the parent counter, so their IDs depend on which promise
// resolves first. On replay, if the order differs, a gets b's checkpointed result
// and b gets a's.
export const wrongHandler = withDurableExecution(async (event: any, context: DurableContext) => {
  const a = context.step(async () => fetchA());
  const b = context.step(async () => fetchB());
  return { a: await a, b: await b };
});

// CORRECT - each branch has its own isolated counter, so IDs are stable regardless
// of completion order
export const handler = withDurableExecution(async (event: any, context: DurableContext) => {
  const a = context.runInChildContext(async (child) => child.step(async () => fetchA()));
  const b = context.runInChildContext(async (child) => child.step(async () => fetchB()));
  return { a: await a, b: await b };
});

async function fetchA() { return "result-a"; }
async function fetchB() { return "result-b"; }

Use parallel or map to run code concurrently.

Warning

In Python, ThreadPoolExecutor.submit and Thread.start do not guarantee the order in which the interpreter actually runs the handler functions.

Use runInChildContextAsync() to get a DurableFuture<T>, then block with DurableFuture.allOf().

import java.util.List;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableFuture;
import software.amazon.lambda.durable.DurableHandler;

public class ConcurrentChildContexts extends DurableHandler<String, String> {

    // WRONG - a and b share the parent counter, so their IDs depend on which future
    // completes first. On replay, if the order differs, a gets b's checkpointed result
    // and b gets a's.
    public String wrongHandleRequest(String input, DurableContext context) {
        DurableFuture<String> a = context.stepAsync("fetch-a", String.class, ctx -> fetchA());
        DurableFuture<String> b = context.stepAsync("fetch-b", String.class, ctx -> fetchB());
        List<String> results = DurableFuture.allOf(a, b);
        return results.get(0) + " " + results.get(1);
    }

    // CORRECT - each branch has its own isolated counter, so IDs are stable regardless
    // of completion order
    @Override
    public String handleRequest(String input, DurableContext context) {
        DurableFuture<String> a = context.runInChildContextAsync(
            "branch-a", String.class, child -> child.step("fetch-a", String.class, ctx -> fetchA())
        );
        DurableFuture<String> b = context.runInChildContextAsync(
            "branch-b", String.class, child -> child.step("fetch-b", String.class, ctx -> fetchB())
        );
        List<String> results = DurableFuture.allOf(a, b);
        return results.get(0) + " " + results.get(1);
    }

    private String fetchA() { return "result-a"; }
    private String fetchB() { return "result-b"; }
}

Testing

The testing SDK records child context operations as CONTEXT type operations. Inspect them to verify the child context ran and produced the expected result.

import { handler } from "./basic-child-context";
import { createTests } from "@aws/durable-execution-sdk-js-testing";
import { OperationType, OperationStatus } from "@aws/durable-execution-sdk-js-testing";

createTests({
  handler,
  tests: (runner) => {
    it("returns the child context result", async () => {
      const execution = await runner.run({ orderId: "order-1" });
      expect(execution.getResult()).toEqual({ orderId: "order-1", charged: true, valid: true });
    });

    it("records a CONTEXT operation", async () => {
      const execution = await runner.run({ orderId: "order-1" });
      const contextOp = runner.getOperationByIndex(0);
      expect(contextOp.getType()).toBe(OperationType.CONTEXT);
      expect(contextOp.getStatus()).toBe(OperationStatus.SUCCEEDED);
    });

    it("child operations are nested under the context operation", async () => {
      const execution = await runner.run({ orderId: "order-1" });
      const contextOp = runner.getOperationByIndex(0);
      expect(contextOp.getChildOperations()).toHaveLength(2);
    });
  },
});
import pytest
from aws_durable_execution_sdk_python.execution import InvocationStatus
from aws_durable_execution_sdk_python_testing.model import OperationType

from examples.python.core.child_contexts.basic_child_context import handler


@pytest.mark.durable_execution(
    handler=handler,
    lambda_function_name="basic-child-context",
)
def test_child_context_succeeds(durable_runner):
    with durable_runner:
        result = durable_runner.run(input={"order_id": "order-1"}, timeout=10)

    assert result.status is InvocationStatus.SUCCEEDED

    context_ops = [op for op in result.operations if op.operation_type is OperationType.CONTEXT]
    assert len(context_ops) >= 1
import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;
import software.amazon.awssdk.services.lambda.model.OperationType;
import software.amazon.lambda.durable.model.ExecutionStatus;
import software.amazon.lambda.durable.testing.LocalDurableTestRunner;

class TestChildContext {

    @Test
    void childContextSucceeds() {
        var handler = new BasicChildContext();
        var runner = LocalDurableTestRunner.create(String.class, handler);

        var result = runner.runUntilComplete("order-1");

        assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
        assertEquals("order-1:validated:charged", result.getResult(String.class));
    }

    @Test
    void recordsContextOperation() {
        var handler = new BasicChildContext();
        var runner = LocalDurableTestRunner.create(String.class, handler);

        var result = runner.runUntilComplete("order-1");

        var contextOps = result.getOperations().stream()
            .filter(op -> op.getType() == OperationType.CONTEXT)
            .toList();
        assertFalse(contextOps.isEmpty());
    }
}

See also