Workflow history propagation in the .NET SDK
Overview
Workflow history propagation allows a parent workflow to share its execution history — and optionally its full ancestor chain —
with child workflows. Call options also expose propagation scopes for activities, but the public .NET reader is
WorkflowContext.GetPropagatedHistory() on a workflow, not a method on WorkflowActivityContext.
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.
Note
This feature requires Dapr .NET SDK v1.18.0 or later and Dapr runtime v1.18.0 or later.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:
public sealed class MerchantCheckoutWorkflow : Workflow<Order, CheckoutResult>
{
public override async Task<CheckoutResult> 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<PaymentResult>(
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:
var options = new WorkflowTaskOptions()
.WithHistoryPropagation(HistoryPropagationScope.OwnHistory);
var auditResult = await context.CallActivityAsync<AuditResult>(
nameof(WriteAuditTrailActivity), payload, options);
Activity reader limitation
In SDK 1.19.0, the activity scheduling action carries this scope, butWorkflowActivityContext exposes only
Identifier, InstanceId, TaskExecutionKey, and ScheduledEventId. It has no public history reader, and the worker does not
populate an activity history property. Use a child workflow when application code needs to inspect propagated
history; do not assume the activity above can call GetPropagatedHistory().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.
public sealed class ProcessPaymentWorkflow : Workflow<Order, PaymentResult>
{
public override async Task<PaymentResult> RunAsync(WorkflowContext context, Order order)
{
var history = context.GetPropagatedHistory();
if (history != null)
{
foreach (var evt in history.Events)
{
// evt.Name, evt.InstanceId, evt.AppId
// evt.Activities — activity results for this ancestor
// evt.Workflows — child workflow results for this ancestor
}
}
return await context.CallActivityAsync<PaymentResult>(
nameof(ChargeCardActivity), order);
}
}
PropagatedHistory type
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 ran this workflow |
InstanceId | string | Workflow instance ID |
Name | string | The name of the workflow |
Activities | IReadOnlyList<PropagatedHistoryActivityResult> | Activity results for this workflow, in execution order |
Workflows | IReadOnlyList<PropagatedHistoryWorkflowResult> | 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? | Serialized input payload, or null when unset |
Output | string? | Serialized output payload, or null when the activity has not completed |
FailureDetails | WorkflowTaskFailureDetails? | Failure details when Status is Failed, otherwise null |
PropagatedHistoryWorkflowResult type
Each PropagatedHistoryWorkflowResult is a sealed record describing a single child workflow invocation:
| Member | Type | Description |
|---|---|---|
Name | string | The scheduled name of the child workflow |
Status | PropagatedHistoryStatus | Lifecycle status — Pending, Completed, or Failed |
Output | string? | Serialized output payload, or null when the workflow has not completed |
FailureDetails | WorkflowTaskFailureDetails? | Failure details when Status is Failed, otherwise null |
Input/output strings retain the configured serialization format; they are JSON only with a JSON serializer. With binary payload support they can also contain reference envelopes. Propagation does not automatically materialize all ancestor payloads; apply compatible serialization and data-reference handling.
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<PropagatedHistoryEvent> | All events from the given Dapr app ID |
GetByInstanceId(string) | IReadOnlyList<PropagatedHistoryEvent> | All events from the given workflow instance ID |
GetEventsByWorkflowName(string) | IReadOnlyList<PropagatedHistoryEvent> | All events with the given workflow name |
TryGetLastWorkflowEventByName(string, out PropagatedHistoryEvent?) | bool | Gets the most recent event matching the workflow name |
GetAppIds() | IReadOnlyList<string> | Ordered, deduplicated list of app IDs in the history |
PropagatedHistoryEvent query methods
Each PropagatedHistoryEvent also provides query methods to inspect the activities and child workflows within that ancestor:
| Method | Return type | Description |
|---|---|---|
GetActivitiesByName(string) | IReadOnlyList<PropagatedHistoryActivityResult> | All activities matching the given name |
TryGetLastActivityByName(string, out PropagatedHistoryActivityResult?) | bool | Gets the most recent activity matching the name |
GetWorkflowsByName(string) | IReadOnlyList<PropagatedHistoryWorkflowResult> | All child workflows matching the given name |
TryGetLastWorkflowByName(string, out PropagatedHistoryWorkflowResult?) | bool | Gets the most recent child workflow matching the name |
Example
var history = context.GetPropagatedHistory();
if (history != null)
{
// By app ID — useful in multi-app workflows
var fromOrderApp = history.GetByAppId("order-app");
// By workflow instance ID
var fromSpecificRun = history.GetByInstanceId("checkout-abc123");
// By workflow name — returns all matches (e.g. recursion or ContinueAsNew)
var checkoutEvents = history.GetEventsByWorkflowName(nameof(MerchantCheckoutWorkflow));
// TryGet for a single match — avoids null ambiguity
if (history.TryGetLastWorkflowEventByName(nameof(MerchantCheckoutWorkflow), out var parentEvent))
{
// Inspect the parent's activities
var failedActivities = parentEvent.Activities
.Where(a => a.Status == PropagatedHistoryStatus.Failed)
.ToList();
// Or look up a specific activity by name
if (parentEvent.TryGetLastActivityByName(nameof(ValidateMerchantActivity), out var validation))
{
// validation.Status, validation.Output, validation.FailureDetails
}
}
}
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 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 for details on configuring WorkflowHistorySigning.