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. Passundefinedto omit.fnAn async function that receives aDurableContextand returnsPromise<T>.config(optional) AChildConfig<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:
funcA callable that receives aDurableContextand returnsT. Use the@durable_with_child_contextdecorator 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) AChildConfigobject.
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. Passnullto omit.resultTypeTheClass<T>orTypeToken<T>for deserialization.funcAFunction<DurableContext, T>to execute.config(optional) ARunInChildContextConfigobject.
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) CustomSerdes<T>for the child context result. See Serialization.subType(optional) An internal subtype identifier. Used bymapandparallelinternally; not needed for direct use.summaryGenerator(optional) A function that generates a compact summary when the result exceeds the checkpoint size limit. Used internally bymapandparallel.errorMapper(optional) A function that maps child context errors to custom error types.virtualContext(optional) Iftrue, 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) CustomSerDesfor the child context result. See Serialization.item_serdes(optional) CustomSerDesfor individual items within the child context.sub_type(optional) An internal subtype identifier. Used bymapandparallelinternally; 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 bymapandparallel.
Parameters:
serDes(optional) CustomSerDesfor 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¶
- All durable operations inside a context must start sequentially.
- To run durable operations concurrently, enclose each set of operations in its own child context.
- Start each child context serially. You do not have to wait for the previous child context to complete before starting the next.
- 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¶
- Steps Run a single function with automatic checkpointing
- Parallel operations Execute operations concurrently
- Map operations Run operation for each item in a collection