diff --git a/.autover/changes/durable-childcontext-virtual.json b/.autover/changes/durable-childcontext-virtual.json
new file mode 100644
index 000000000..af46ffd0a
--- /dev/null
+++ b/.autover/changes/durable-childcontext-virtual.json
@@ -0,0 +1,11 @@
+{
+ "Projects": [
+ {
+ "Name": "Amazon.Lambda.DurableExecution",
+ "Type": "Minor",
+ "ChangelogMessages": [
+ "Add virtual-context support to standalone RunInChildContextAsync via ChildContextConfig.NestingType. Setting NestingType.Flat runs the child in a virtual context that emits no CONTEXT checkpoint of its own (mirroring MapConfig/ParallelConfig), enabling manual fan-out with Task.WhenAll while reducing checkpoint volume."
+ ]
+ }
+ ]
+}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution/ChildContextConfig.cs b/Libraries/src/Amazon.Lambda.DurableExecution/ChildContextConfig.cs
index c00adf909..b2a5ec301 100644
--- a/Libraries/src/Amazon.Lambda.DurableExecution/ChildContextConfig.cs
+++ b/Libraries/src/Amazon.Lambda.DurableExecution/ChildContextConfig.cs
@@ -32,4 +32,26 @@ public sealed class ChildContextConfig
/// is mapped before being thrown).
///
public Func? ErrorMapping { get; set; }
+
+ ///
+ /// How the child context is represented in the checkpoint graph. Defaults to
+ /// .
+ ///
+ ///
+ ///
+ /// Under the child runs in a virtual context
+ /// that emits no CONTEXT checkpoint of its own — reducing checkpoint
+ /// volume at the cost of less granular execution traces. Operations inside
+ /// the child (steps, waits, callbacks) still checkpoint, re-parented to this
+ /// context's parent.
+ ///
+ ///
+ /// This mirrors the NestingType.Flat option on
+ /// and , letting a
+ /// standalone
+ /// call opt into virtual contexts — for example when manually fanning out
+ /// work and awaiting the returned tasks with Task.WhenAll.
+ ///
+ ///
+ public NestingType NestingType { get; set; } = NestingType.Nested;
}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution/DurableContext.cs b/Libraries/src/Amazon.Lambda.DurableExecution/DurableContext.cs
index 5ff6c1bab..8dfabba19 100644
--- a/Libraries/src/Amazon.Lambda.DurableExecution/DurableContext.cs
+++ b/Libraries/src/Amazon.Lambda.DurableExecution/DurableContext.cs
@@ -167,7 +167,8 @@ private Task RunChildContext(
var op = new ChildContextOperation(
operationId, name, _idGenerator.ParentId, func, config, serializer, MakeChildFactory(),
- _state, _terminationManager, _workflowCancellation, _durableExecutionArn, _batcher);
+ _state, _terminationManager, _workflowCancellation, _durableExecutionArn, _batcher,
+ isVirtual: config?.NestingType == NestingType.Flat);
return op.ExecuteAsync(cancellationToken);
}
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.Tests/ChildContextOperationTests.cs b/Libraries/test/Amazon.Lambda.DurableExecution.Tests/ChildContextOperationTests.cs
index 1782fe933..2559760ea 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.Tests/ChildContextOperationTests.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.Tests/ChildContextOperationTests.cs
@@ -610,4 +610,107 @@ await context.RunInChildContextAsync(
}
}
+ [Fact]
+ public async Task RunInChildContextAsync_FlatNesting_EmitsNoContextCheckpointAndReparentsInnerOps()
+ {
+ var (context, recorder, tm, _) = CreateContext();
+
+ var executed = false;
+ var result = await context.RunInChildContextAsync(
+ async (childCtx, _) =>
+ {
+ executed = true;
+ return await childCtx.StepAsync(async (_, _) => { await Task.CompletedTask; return "inner"; }, name: "inner_step");
+ },
+ name: "phase",
+ config: new ChildContextConfig { NestingType = NestingType.Flat });
+
+ Assert.True(executed);
+ Assert.Equal("inner", result);
+ Assert.False(tm.IsTerminated);
+
+ await recorder.Batcher.DrainAsync();
+
+ // A virtual (Flat) child emits no CONTEXT checkpoint of its own — only
+ // the inner step's operations are recorded.
+ var actions = recorder.Flushed.Select(o => $"{o.Type}:{o.Action}").ToArray();
+ Assert.Equal(new[]
+ {
+ "STEP:START",
+ "STEP:SUCCEED"
+ }, actions);
+ Assert.DoesNotContain(recorder.Flushed, o => o.Type == "CONTEXT");
+
+ // Inner-op IDs still derive from the child's own operation ID (so sibling
+ // branches never collide), but they re-parent to the nearest non-virtual
+ // ancestor — here the root, whose ParentId is null — since the virtual
+ // branch has no CONTEXT checkpoint for them to reference.
+ var parentOpId = IdAt(1);
+ var innerStepId = ChildIdAt(parentOpId, 1);
+ var stepStart = recorder.Flushed.Single(o => o.Type == "STEP" && o.Action == "START");
+ Assert.Equal(innerStepId, stepStart.Id);
+ Assert.Null(stepStart.ParentId);
+ }
+
+ [Fact]
+ public async Task RunInChildContextAsync_FlatNesting_ReplayReExecutesBodyFromInnerCheckpoint()
+ {
+ // No CONTEXT checkpoint exists for a virtual child, so on replay the body
+ // re-executes; the inner step replays from its own SUCCEEDED checkpoint
+ // without re-running the step delegate.
+ var parentOpId = IdAt(1);
+ var innerStepId = ChildIdAt(parentOpId, 1);
+ var (context, recorder, _, _) = CreateContext(new InitialExecutionState
+ {
+ Operations = new List
+ {
+ new()
+ {
+ Id = innerStepId,
+ Type = OperationTypes.Step,
+ Status = OperationStatuses.Succeeded,
+ Name = "inner_step",
+ StepDetails = new StepDetails { Result = "\"cached\"" }
+ }
+ }
+ });
+
+ var stepRan = false;
+ var result = await context.RunInChildContextAsync(
+ async (childCtx, _) =>
+ await childCtx.StepAsync(async (_, _) =>
+ {
+ stepRan = true;
+ await Task.CompletedTask;
+ return "fresh";
+ }, name: "inner_step"),
+ name: "phase",
+ config: new ChildContextConfig { NestingType = NestingType.Flat });
+
+ Assert.False(stepRan);
+ Assert.Equal("cached", result);
+
+ await recorder.Batcher.DrainAsync();
+ Assert.DoesNotContain(recorder.Flushed, o => o.Type == "CONTEXT");
+ }
+
+ [Fact]
+ public async Task RunInChildContextAsync_FlatNesting_FuncThrows_EmitsNoFailCheckpointButPropagates()
+ {
+ var (context, recorder, _, _) = CreateContext();
+
+ var ex = await Assert.ThrowsAsync(() =>
+ context.RunInChildContextAsync(
+ (_, _) => throw new InvalidOperationException("boom"),
+ name: "phase",
+ config: new ChildContextConfig { NestingType = NestingType.Flat }));
+
+ Assert.Equal("boom", ex.Message);
+
+ await recorder.Batcher.DrainAsync();
+ // Virtual child suppresses the CONTEXT FAIL checkpoint; the exception
+ // still propagates to the caller.
+ Assert.DoesNotContain(recorder.Flushed, o => o.Type == "CONTEXT");
+ }
+
}