This plan implements a comprehensive call stack mechanism to track the execution chain from root workflow through all parent activities to a specific activity execution, enabling visibility into the complete invocation hierarchy when viewing activity execution records.
Core design
- Explicit predecessor: Activities that know the causal predecessor (e.g., a completed child that schedules the next) set
SchedulingActivityExecutionIddirectly via scheduling options. - Ambient fallback: During completion callbacks, bookmark resumes, and child-workflow starts, the workflow sets an ambient "current scheduling source" on the
WorkflowExecutionContext. If a schedule call omitsSchedulingActivityExecutionId, the scheduler fills it from the ambient. This minimizes code churn while preserving correctness. - Structural vs temporal: Keep
Owner/ParentActivityExecutionContextfor structural containment; useSchedulingActivityExecutionId/SchedulingWorkflowInstanceIdfor the temporal execution chain.
Add SchedulingActivityExecutionId (nullable string) to:
ScheduleWorkOptionsScheduledActivityOptionsActivityWorkItemActivityInvocationOptions
Include clear XML documentation explaining this tracks the temporal/execution predecessor (distinct from structural Owner/ParentActivityExecutionContext).
Update all constructors and property mappings in:
WorkflowExecutionContextSchedulerStrategy.ScheduleDefaultActivitySchedulerMiddleware.ExecuteWorkItemAsync
Thread this value through the scheduling chain.
Add the following fields to ActivityExecutionContext:
SchedulingActivityExecutionId(nullablestring)SchedulingActivityId(nullablestring- denormalized for convenience)SchedulingWorkflowInstanceId(nullablestring- for cross-workflow tracking)
Include XML comments distinguishing these from ParentActivityExecutionContext:
ParentActivityExecutionContext: The structural container activity (e.g., Flowchart contains all its children). Represents the hierarchical parent in the workflow structure.SchedulingActivityExecutionId: The temporal/execution predecessor that directly triggered execution of this activity. Tracks the execution sequence, not the structural hierarchy.SchedulingWorkflowInstanceId: The workflow instance ID of the activity that invoked this activity's workflow. Set when crossing workflow boundaries (e.g., viaExecuteWorkfloworDispatchWorkflow).
Update WorkflowExecutionContext.CreateActivityExecutionContextAsync to accept and store these fields from ActivityInvocationOptions.
Add corresponding fields to ActivityExecutionRecord:
SchedulingActivityExecutionId(nullablestring)SchedulingActivityId(nullablestring)SchedulingWorkflowInstanceId(nullablestring)CallStackDepth(nullableint)
Update DefaultActivityExecutionMapper.MapAsync to populate these fields from the execution context. Calculate CallStackDepth by traversing the SchedulingActivityExecutionId chain until reaching null.
Add an ambient scheduling source to [WorkflowExecutionContext]:
- Fields (transient):
CurrentSchedulingActivityExecutionId,CurrentSchedulingWorkflowInstanceId. - API:
IDisposable BeginSchedulingScope(string? activityExecutionId, string? workflowInstanceId)that pushes values and restores previous values on dispose.
Set ambient scope in these places:
- Around owner completion callbacks (where next activities are scheduled).
- Around bookmark-resume handlers (background completions resuming the workflow).
- At child-workflow start (root activity creation).
Modify [WorkflowExecutionContextSchedulerStrategy.Schedule] to set SchedulingActivityExecutionId and SchedulingWorkflowInstanceId from ScheduleWorkOptions if provided; otherwise fall back to the ambient WorkflowExecutionContext values.
In Flowchart.Counters.cs:
- Update
ScheduleOutboundActivityAsyncto populateScheduleWorkOptions.SchedulingActivityExecutionId = completedActivityContext.Idwhen a completed activity schedules its outbound activities. - Update
MaybeScheduleBackwardConnectionActivityAsyncto includeSchedulingActivityExecutionIdin theScheduleWorkOptions. - Update
MaybeScheduleWaitAllActivityAsync,MaybeScheduleWaitAllActiveActivityAsync,MaybeScheduleWaitAnyActivityAsyncto passSchedulingActivityExecutionIdwhen scheduling.
In Flowchart.Tokens.cs:
- Update
OnChildCompletedTokenBasedLogicAsyncto passSchedulingActivityExecutionIdinScheduleWorkOptionswhen scheduling subsequent activities.
Apply the same pattern to other composite activities:
SequenceForEachParallelWhileDo- Any other activities that schedule child activities based on completion
When scheduling a child activity that was directly triggered by another activity's completion, set SchedulingActivityExecutionId to the completing activity's execution context ID. When omitted, the ambient scope ensures a sensible fallback.
For ExecuteWorkflow and DispatchWorkflow:
- Capture the calling activity's
ExecutionIdand currentWorkflowInstanceId. - Pass them through
RunWorkflowOptionsandDispatchWorkflowRequestto the child workflow. - When the child workflow starts, set the first activity's:
SchedulingActivityExecutionIdto the parent's invocation activity's execution ID.SchedulingWorkflowInstanceIdto the parent workflow instance ID.
- Also set the ambient scope (
BeginSchedulingScope) for the duration of child start so subsequent schedules inherit these values by default.
Cross-workflow chains should be considered part of the call stack by default (span by default).
Add CallStackDepth (nullable int) field to ActivityExecutionRecord.
In DefaultActivityExecutionMapper.MapAsync:
- Calculate
CallStackDepthby traversing the sourceActivityExecutionContext.SchedulingActivityExecutionIdchain until reaching null. - Use root depth = 0 (documented convention).
- Store this value in the
ActivityExecutionRecord.
Create EF Core migration adding indexed columns to ActivityExecutionRecord table:
SchedulingActivityExecutionId(indexed, nullable)SchedulingActivityId(indexed, nullable)SchedulingWorkflowInstanceId(indexed, nullable)CallStackDepth(indexed, nullable)
The CallStackDepth index enables efficient filtering by execution depth without reconstructing the full chain (e.g., "show me all activities at depth > 5").
Implement IActivityExecutionStore.GetExecutionChainAsync(string activityExecutionId, bool includeCrossWorkflowChain = true, int? skip = null, int? take = null) that:
- Recursively queries
SchedulingActivityExecutionIduntil reaching root (null). - Follows
SchedulingWorkflowInstanceIdacross workflow boundaries by default (span by default). Optionally allow disabling cross-workflow span via parameter. - Supports pagination via
skipandtakeparameters to handle deep call stacks efficiently. - Returns a paginated result containing:
Items: List of execution records (ordered from root to current activity, or subset if paginated)TotalCount: Total number of items in the full chainSkip: The skip value usedTake: The take value used
- When pagination is not specified (
skipandtakeare null), returns the full chain.
Add extension methods:
ActivityExecutionContext.GetExecutionChain(int? skip = null, int? take = null): Reconstruct the runtime call stack by traversingSchedulingActivityExecutionId, returning a paginated result from root to current activity.ActivityExecutionRecord.GetExecutionChainAsync(IActivityExecutionStore, bool includeCrossWorkflowChain = true, int? skip = null, int? take = null): Reconstruct persisted call stacks by querying the store with pagination support.
Both methods should return results ordered from root to current activity, with pagination applied after ordering.
Add REST API endpoint:
GET /api/workflow-instances/{workflowInstanceId}/activity-executions/{activityExecutionId}/call-chain- Query parameters:
includeCrossWorkflowChain(bool, default: true): Include parent workflow activities across workflow boundariesskip(int?, optional): Number of items to skip (for pagination)take(int?, optional): Number of items to return (for pagination, recommended max: 100)
- Response:
items: Array of activity execution recordstotalCount: Total number of items in the full chainskip: The skip value usedtake: The take value used (or null if full chain returned)
- This enables UI to implement paginated/lazy loading for deep call stacks.
- Query parameters:
ParentActivityExecutionContext represents the structural container (e.g., Flowchart contains all its children), while SchedulingActivityExecutionId tracks the temporal execution predecessor (e.g., Activity B completed and directly triggered Activity C).
These are orthogonal relationships:
- A Flowchart can own many children (structural), but only a predecessor directly triggers the next (temporal).
- When Activity B completes, it schedules the next child, establishing a temporal link via
SchedulingActivityExecutionId.
All XML comments for these fields should explicitly clarify this distinction to prevent developer confusion and misuse.
- The ambient scope must be short-lived and always disposed via
using/finallyto avoid leakage between unrelated scheduling operations. - The scheduler should prefer explicit
ScheduleWorkOptions.SchedulingActivityExecutionId/SchedulingWorkflowInstanceIdand only fall back to ambient when not provided. - Document that the ambient exists to reduce boilerplate and should not be relied upon when explicit causal context is readily available.
SchedulingWorkflowInstanceId enables reconstructing call stacks that span multiple workflow instances:
- Parent Workflow (Instance A) → ExecuteWorkflow activity (in Instance A) → Child Workflow (Instance B) → failing activity (in Instance B).
Since span is the default, cross-instance traversal should occur unless explicitly disabled.
Storing CallStackDepth trades a small amount of storage for simpler, faster queries and analytics:
Benefits:
- Efficient filtering by depth ranges (e.g., "depth > 5").
- Early termination in chain reconstruction.
- Index-based analytics queries.
- Lower storage than persisting full chains.
Drawbacks:
CallStackDepthbecomes stale if parent records are deleted or altered post-hoc. Prefer immutable execution records.- Document that
CallStackDepthis an optimization hint for querying, not an authoritative source if retention policies prune ancestors.
- Sequential flow: A → B → C (explicit predecessor set, ambient unused).
- Parallel fan-out: A schedules B and C (both record A as predecessor; ambient vs explicit).
- Nested composites: Multiple owners scheduling into the same queue.
- Background resume: Bookmark-based resumes interleaving with other work; ambient set during resume.
- Cross-workflow: Execute/Dispatch child workflow; default spanning chain.
- Deduplication scenarios:
PreventDuplicateSchedulingand re-scheduling. - Persistence/round-trips: Background/persisted scheduled activities using
ScheduledActivityOptions. - Deep call stacks: Test pagination with chains deeper than 100 activities.
- Cross-workflow pagination: Ensure pagination works correctly when spanning workflow boundaries.
Deep call stack handling:
- For very deep call stacks (e.g., recursive workflows or long-running sequential processes), retrieving the entire chain in a single query can be expensive.
- Pagination (
skip/take) enables efficient loading in the UI with incremental/lazy loading patterns. - Recommend default
takeof 50-100 items per page for REST API calls. - The
CallStackDepthfield enables quick assessment of chain depth before deciding whether to paginate.
Query optimization strategies:
- Use indexed lookups on
SchedulingActivityExecutionIdto traverse the chain efficiently. - Consider caching strategies for frequently accessed chains (e.g., recently failed activities).
- For cross-workflow queries, implement efficient join strategies or batched lookups to minimize round-trips.
- Document that pagination is "forward-only" (skip/take from root toward current) to align with typical debugging workflows (start at root, drill down).
REST API rate limiting:
- Consider rate limiting on the call chain endpoint if it becomes a performance bottleneck.
- Monitor query performance and adjust default pagination sizes based on observed data.