From f50eddc7bd0f56aac4b3db613ff64cceb8e9ded2 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 19 May 2026 12:16:13 -0500 Subject: [PATCH 1/2] Adding .NET SDK documentation for history propagation Signed-off-by: Whit Waldo --- .../dotnet-workflow-history-propagation.md | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 sdkdocs/dotnet/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-history-propagation.md diff --git a/sdkdocs/dotnet/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-history-propagation.md b/sdkdocs/dotnet/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-history-propagation.md new file mode 100644 index 00000000000..1fb8b850421 --- /dev/null +++ b/sdkdocs/dotnet/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-history-propagation.md @@ -0,0 +1,160 @@ +--- +type: docs +title: "Workflow history propagation in the .NET SDK" +linkTitle: "History propagation" +weight: 1700 +description: Share ancestor workflow execution history with child workflows and activities using the .NET SDK +--- + +## Overview + +Workflow history propagation allows a parent workflow to share its execution history — and optionally its full ancestor chain — with the child workflows and activities it calls. The child can then inspect those upstream events at runtime. + +Common use cases include: + +- Audit trails: Verifying a chain of custody across a multi-step workflow +- Fraud detection: Inspecting upstream decisions before committing a transaction +- AI agent orchestration: Passing context through hierarchical agent workflows + +Conceptual guidance is covered in [Workflow history propagation]({{% ref "workflow-history-propagation.md" %}}). + +{{% alert title="Note" color="primary" %}} +This feature requires Dapr .NET SDK v1.18.0 or later and Dapr runtime v1.18.0 or later. +{{% /alert %}} + +## Propagation scopes + +Propagation is **opt-in and per-call**. Each call to `CallActivityAsync` or `CallChildWorkflowAsync` can independently specify a `HistoryPropagationScope`: + +| Scope | Description | +|---|---| +| `None` | Default. No history is propagated to the callee. | +| `OwnHistory` | Propagates the calling workflow's own events only. Ancestor history is dropped, acting as a trust boundary. | +| `Lineage` | Propagates the calling workflow's events plus the full ancestor chain it inherited from its own parent. | + +## Propagate history to a child workflow + +Use `WithHistoryPropagation` on `ChildWorkflowTaskOptions` to opt a child workflow into receiving the parent's history: + +```csharp +public sealed class MerchantCheckoutWorkflow : Workflow +{ + public override async Task RunAsync(WorkflowContext context, Order order) + { + // Activity without propagation — default behavior, no opt-in + await context.CallActivityAsync(nameof(ValidateMerchantActivity), order.MerchantId); + + // Child workflow with full lineage propagation + var options = new ChildWorkflowTaskOptions() + .WithHistoryPropagation(HistoryPropagationScope.Lineage); + + var result = await context.CallChildWorkflowAsync( + nameof(ProcessPaymentWorkflow), order, options); + + return new CheckoutResult(result); + } +} +``` + +Calls that do not specify a propagation scope receive no history — other calls in the same workflow are unaffected by opt-ins. + +## Propagate history to an activity + +The same `WithHistoryPropagation` extension is available on `WorkflowTaskOptions` for activity calls: + +```csharp +var options = new WorkflowTaskOptions() + .WithHistoryPropagation(HistoryPropagationScope.OwnHistory); + +var auditResult = await context.CallActivityAsync( + nameof(WriteAuditTrailActivity), payload, options); +``` + +## Read propagated history + +Inside a child workflow, call `GetPropagatedHistory()` on `WorkflowContext` to retrieve the history passed by the parent. The method returns `null` if propagation was not requested for this invocation. + +```csharp +public sealed class ProcessPaymentWorkflow : Workflow +{ + public override async Task RunAsync(WorkflowContext context, Order order) + { + var history = context.GetPropagatedHistory(); + + if (history != null) + { + foreach (var entry in history.Entries) + { + // entry.WorkflowName, entry.InstanceId, entry.AppId, entry.Events + } + } + + return await context.CallActivityAsync( + nameof(ChargeCardActivity), order); + } +} +``` + +### PropagatedHistory type + +`GetPropagatedHistory()` returns a `PropagatedHistory` object (or `null`). Its `Entries` property is a collection of `PropagatedHistoryEntry` records, each representing one workflow in the ancestor chain: + +| Member | Type | Description | +|---|---|---| +| `AppId` | `string` | Dapr app ID that hosted the workflow | +| `InstanceId` | `string` | Workflow instance ID | +| `WorkflowName` | `string` | Registered name of the workflow | +| `Events` | `IReadOnlyList` | History events for this workflow | + +Each `PropagatedHistoryEvent` has: + +| Member | Type | Description | +|---|---|---| +| `EventId` | `int` | Sequence number within the workflow history | +| `Kind` | `HistoryEventKind` | The type of event (see below) | +| `Timestamp` | `DateTimeOffset` | When the event occurred | + +`HistoryEventKind` values include `ExecutionStarted`, `ExecutionCompleted`, `TaskScheduled`, `TaskCompleted`, `TaskFailed`, `SubOrchestrationInstanceCreated`, `SubOrchestrationInstanceCompleted`, `SubOrchestrationInstanceFailed`, `TimerCreated`, `TimerFired`, `OrchestratorStarted`, `OrchestratorCompleted`, `EventSent`, `EventRaised`, `ContinueAsNew`, `ExecutionSuspended`, and `ExecutionResumed`. + +## Filter propagated history + +`PropagatedHistory` provides filter methods to narrow the entries to a specific workflow in the chain: + +```csharp +var history = context.GetPropagatedHistory(); + +if (history != null) +{ + // By app ID — useful in multi-app workflows + var fromOrderApp = history.FilterByAppId("order-app"); + + // By workflow instance ID + var fromSpecificRun = history.FilterByInstanceId("checkout-abc123"); + + // By workflow name + var checkoutEntries = history.FilterByWorkflowName(nameof(MerchantCheckoutWorkflow)); + + foreach (var entry in checkoutEntries) + { + var failedTasks = entry.Events + .Where(e => e.Kind == HistoryEventKind.TaskFailed) + .ToList(); + + // Use failedTasks for audit or routing decisions + } +} +``` + +## Security considerations + +By default, Dapr uses mutual TLS (mTLS) between sidecars for all cross-app communication, providing transport-layer protection for propagated history in [multi-app workflow]({{% ref "dotnet-workflow-multi-app.md" %}}) scenarios. + +For stronger guarantees in production, enable `WorkflowHistorySigning`. This feature uses SPIFFE identity to cryptographically sign each history chunk, so the receiving workflow can verify the integrity and origin of the propagated history. Without signing enabled, Dapr emits a warning that propagated chunks lack cryptographic verification. + +See [Workflow history propagation]({{% ref "workflow-history-propagation.md" %}}) for details on configuring `WorkflowHistorySigning`. + +## Next steps + +- [Workflow history propagation]({{% ref "workflow-history-propagation.md" %}}) +- [Multi-app workflows]({{% ref "dotnet-workflow-multi-app.md" %}}) +- [Workflow management operations]({{% ref "dotnet-workflow-management-methods.md" %}}) From f997c591d0db8d8f9a2acd689b3a89a925bd1905 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Thu, 28 May 2026 15:28:42 -0500 Subject: [PATCH 2/2] Updated .NET SDK documentation to reflect latest changes Signed-off-by: Whit Waldo --- .../dotnet-workflow-history-propagation.md | 99 +++++++++++++++---- 1 file changed, 78 insertions(+), 21 deletions(-) diff --git a/sdkdocs/dotnet/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-history-propagation.md b/sdkdocs/dotnet/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-history-propagation.md index 1fb8b850421..f43a73fad82 100644 --- a/sdkdocs/dotnet/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-history-propagation.md +++ b/sdkdocs/dotnet/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-history-propagation.md @@ -83,9 +83,11 @@ public sealed class ProcessPaymentWorkflow : Workflow if (history != null) { - foreach (var entry in history.Entries) + foreach (var evt in history.Events) { - // entry.WorkflowName, entry.InstanceId, entry.AppId, entry.Events + // evt.Name, evt.InstanceId, evt.AppId + // evt.Activities — activity results for this ancestor + // evt.Workflows — child workflow results for this ancestor } } @@ -97,28 +99,77 @@ public sealed class ProcessPaymentWorkflow : Workflow ### PropagatedHistory type -`GetPropagatedHistory()` returns a `PropagatedHistory` object (or `null`). Its `Entries` property is a collection of `PropagatedHistoryEntry` records, each representing one workflow in the ancestor chain: +`GetPropagatedHistory()` returns a `PropagatedHistory` object (or `null`). Its `Events` property is an ordered list of `PropagatedHistoryEvent` values — one per ancestor workflow, in execution order (oldest ancestor first, immediate parent last). + +Each `PropagatedHistoryEvent` represents a single ancestor workflow's contribution to the propagated history: | Member | Type | Description | |---|---|---| -| `AppId` | `string` | Dapr app ID that hosted the workflow | +| `AppId` | `string` | Dapr app ID that ran this workflow | | `InstanceId` | `string` | Workflow instance ID | -| `WorkflowName` | `string` | Registered name of the workflow | -| `Events` | `IReadOnlyList` | History events for this workflow | +| `Name` | `string` | The name of the workflow | +| `Activities` | `IReadOnlyList` | Activity results for this workflow, in execution order | +| `Workflows` | `IReadOnlyList` | Child workflow results for this workflow, in execution order | + +### PropagatedHistoryActivityResult type + +Each `PropagatedHistoryActivityResult` is a sealed record describing a single activity invocation: + +| Member | Type | Description | +|---|---|---| +| `Name` | `string` | The scheduled name of the activity | +| `Status` | `PropagatedHistoryStatus` | Lifecycle status — `Pending`, `Completed`, or `Failed` | +| `Input` | `string?` | JSON-encoded input payload, or `null` when unset | +| `Output` | `string?` | JSON-encoded output payload, or `null` when the activity has not completed | +| `FailureDetails` | `WorkflowTaskFailureDetails?` | Failure details when `Status` is `Failed`, otherwise `null` | + +### PropagatedHistoryWorkflowResult type -Each `PropagatedHistoryEvent` has: +Each `PropagatedHistoryWorkflowResult` is a sealed record describing a single child workflow invocation: | Member | Type | Description | |---|---|---| -| `EventId` | `int` | Sequence number within the workflow history | -| `Kind` | `HistoryEventKind` | The type of event (see below) | -| `Timestamp` | `DateTimeOffset` | When the event occurred | +| `Name` | `string` | The scheduled name of the child workflow | +| `Status` | `PropagatedHistoryStatus` | Lifecycle status — `Pending`, `Completed`, or `Failed` | +| `Output` | `string?` | JSON-encoded output payload, or `null` when the workflow has not completed | +| `FailureDetails` | `WorkflowTaskFailureDetails?` | Failure details when `Status` is `Failed`, otherwise `null` | + +### PropagatedHistoryStatus enum + +`PropagatedHistoryStatus` reflects how far a task progressed past scheduling: + +| Value | Description | +|---|---| +| `Pending` | The task was scheduled but has not yet completed or failed | +| `Completed` | The task completed successfully | +| `Failed` | The task failed | + +## Query propagated history + +### PropagatedHistory query methods + +`PropagatedHistory` provides `Get` methods that return lists and `TryGet` methods that return a single match (the most recent) via an `out` parameter. All `Get` methods return an empty list when no match is found. + +| Method | Return type | Description | +|---|---|---| +| `GetByAppId(string)` | `IReadOnlyList` | All events from the given Dapr app ID | +| `GetByInstanceId(string)` | `IReadOnlyList` | All events from the given workflow instance ID | +| `GetEventsByWorkflowName(string)` | `IReadOnlyList` | All events with the given workflow name | +| `TryGetLastWorkflowEventByName(string, out PropagatedHistoryEvent?)` | `bool` | Gets the most recent event matching the workflow name | +| `GetAppIds()` | `IReadOnlyList` | Ordered, deduplicated list of app IDs in the history | + +### PropagatedHistoryEvent query methods -`HistoryEventKind` values include `ExecutionStarted`, `ExecutionCompleted`, `TaskScheduled`, `TaskCompleted`, `TaskFailed`, `SubOrchestrationInstanceCreated`, `SubOrchestrationInstanceCompleted`, `SubOrchestrationInstanceFailed`, `TimerCreated`, `TimerFired`, `OrchestratorStarted`, `OrchestratorCompleted`, `EventSent`, `EventRaised`, `ContinueAsNew`, `ExecutionSuspended`, and `ExecutionResumed`. +Each `PropagatedHistoryEvent` also provides query methods to inspect the activities and child workflows within that ancestor: -## Filter propagated history +| Method | Return type | Description | +|---|---|---| +| `GetActivitiesByName(string)` | `IReadOnlyList` | All activities matching the given name | +| `TryGetLastActivityByName(string, out PropagatedHistoryActivityResult?)` | `bool` | Gets the most recent activity matching the name | +| `GetWorkflowsByName(string)` | `IReadOnlyList` | All child workflows matching the given name | +| `TryGetLastWorkflowByName(string, out PropagatedHistoryWorkflowResult?)` | `bool` | Gets the most recent child workflow matching the name | -`PropagatedHistory` provides filter methods to narrow the entries to a specific workflow in the chain: +### Example ```csharp var history = context.GetPropagatedHistory(); @@ -126,21 +177,27 @@ var history = context.GetPropagatedHistory(); if (history != null) { // By app ID — useful in multi-app workflows - var fromOrderApp = history.FilterByAppId("order-app"); + var fromOrderApp = history.GetByAppId("order-app"); // By workflow instance ID - var fromSpecificRun = history.FilterByInstanceId("checkout-abc123"); + var fromSpecificRun = history.GetByInstanceId("checkout-abc123"); - // By workflow name - var checkoutEntries = history.FilterByWorkflowName(nameof(MerchantCheckoutWorkflow)); + // By workflow name — returns all matches (e.g. recursion or ContinueAsNew) + var checkoutEvents = history.GetEventsByWorkflowName(nameof(MerchantCheckoutWorkflow)); - foreach (var entry in checkoutEntries) + // TryGet for a single match — avoids null ambiguity + if (history.TryGetLastWorkflowEventByName(nameof(MerchantCheckoutWorkflow), out var parentEvent)) { - var failedTasks = entry.Events - .Where(e => e.Kind == HistoryEventKind.TaskFailed) + // Inspect the parent's activities + var failedActivities = parentEvent.Activities + .Where(a => a.Status == PropagatedHistoryStatus.Failed) .ToList(); - // Use failedTasks for audit or routing decisions + // Or look up a specific activity by name + if (parentEvent.TryGetLastActivityByName(nameof(ValidateMerchantActivity), out var validation)) + { + // validation.Status, validation.Output, validation.FailureDetails + } } } ```