|
| 1 | +--- |
| 2 | +title: Workflow Execution Modes |
| 3 | +description: Deep dive into the OffThread and Lockstep execution modes for .NET workflows. |
| 4 | +zone_pivot_groups: programming-languages |
| 5 | +author: TaoChenOSU |
| 6 | +ms.topic: conceptual |
| 7 | +ms.author: taochen |
| 8 | +ms.date: 03/18/2026 |
| 9 | +ms.service: agent-framework |
| 10 | +--- |
| 11 | + |
| 12 | +<!-- |
| 13 | + Language parity table – keep in sync when adding/removing sections. |
| 14 | +
|
| 15 | + | Section | C# | Python | Notes | |
| 16 | + |------------------------|:--:|:------:|----------------| |
| 17 | + | Overview | ✅ | ❌ | C#-specific | |
| 18 | + | OffThread | ✅ | ❌ | C#-specific | |
| 19 | + | Lockstep | ✅ | ❌ | C#-specific | |
| 20 | + | Choosing a Mode | ✅ | ❌ | C#-specific | |
| 21 | + | Non-Streaming | ✅ | ❌ | C#-specific | |
| 22 | + | Not applicable notice | ❌ | ✅ | Python-specific | |
| 23 | +--> |
| 24 | + |
| 25 | +# Workflow Execution Modes |
| 26 | + |
| 27 | +::: zone pivot="programming-language-csharp" |
| 28 | + |
| 29 | +When running a workflow in .NET, the **execution mode** controls how supersteps are processed and how events are delivered to the consumer. The `InProcessExecution` class exposes two execution modes: **OffThread** and **Lockstep**. |
| 30 | + |
| 31 | +## Overview |
| 32 | + |
| 33 | +| | OffThread (Default) | Lockstep | |
| 34 | +|---|---|---| |
| 35 | +| **Superstep execution** | Background thread | Consumer's thread | |
| 36 | +| **Event delivery** | Immediate, as events are raised | Batched after each superstep completes | |
| 37 | +| **Step execution** | Independent of event processing | Paused until batched events are consumed | |
| 38 | +| **Concurrency** | Consumer reads events while supersteps run | Consumer and superstep execution alternate | |
| 39 | +| **Best for** | Real-time streaming, production scenarios | Testing, debugging, deterministic ordering | |
| 40 | + |
| 41 | +## OffThread |
| 42 | + |
| 43 | +OffThread is the **default** execution mode. Supersteps run on a background thread, and events stream out immediately as they are raised via a channel-based implementation. |
| 44 | + |
| 45 | +```csharp |
| 46 | +// OffThread is the default — these are equivalent: |
| 47 | +await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, input); |
| 48 | +await using StreamingRun run = await InProcessExecution.OffThread.RunStreamingAsync(workflow, input); |
| 49 | +``` |
| 50 | + |
| 51 | +### How it works |
| 52 | + |
| 53 | +1. A background task runs supersteps continuously while messages are pending. |
| 54 | +2. As executors yield outputs or events, the resulting `WorkflowEvent` objects are written to an unbounded `Channel<WorkflowEvent>`. |
| 55 | +3. The consumer reads events from the channel via `WatchStreamAsync`, receiving them in real-time as they are produced. |
| 56 | +4. When all supersteps are complete and no messages remain, the run halts with an `Idle` or `PendingRequests` status. |
| 57 | + |
| 58 | +Because the superstep loop and the consumer run concurrently, events appear as soon as they are raised — there is no buffering delay. This makes OffThread ideal for streaming scenarios where low-latency event delivery matters, such as displaying token-by-token updates in a UI. |
| 59 | + |
| 60 | +### Concurrent runs |
| 61 | + |
| 62 | +OffThread also supports a **concurrent** variant that allows multiple runs to share the same workflow instance simultaneously: |
| 63 | + |
| 64 | +```csharp |
| 65 | +await using StreamingRun run = await InProcessExecution.Concurrent.RunStreamingAsync(workflow, input); |
| 66 | +``` |
| 67 | + |
| 68 | +> [!IMPORTANT] |
| 69 | +> Concurrent execution requires that all executors in the workflow be declared `crossRunShareable` (on the constructor) or be provided as factory methods. |
| 70 | +
|
| 71 | +## Lockstep |
| 72 | + |
| 73 | +In Lockstep mode, supersteps run in the **consumer's thread** rather than on a background task. Events are accumulated during each superstep and emitted as a batch after the superstep completes. |
| 74 | + |
| 75 | +```csharp |
| 76 | +await using StreamingRun run = await InProcessExecution.Lockstep.RunStreamingAsync(workflow, input); |
| 77 | +``` |
| 78 | + |
| 79 | +### How it works |
| 80 | + |
| 81 | +1. The consumer calls `WatchStreamAsync`, which drives the execution loop. |
| 82 | +2. A superstep runs to completion, and events are accumulated in a queue. |
| 83 | +3. After the superstep finishes, all queued events are yielded to the consumer. |
| 84 | +4. The next superstep begins only after the consumer has received all events from the previous one. |
| 85 | + |
| 86 | +This alternating pattern means the consumer and the workflow engine never run simultaneously. Event delivery is deterministic — all events from a superstep are guaranteed to arrive before any events from the next superstep. |
| 87 | + |
| 88 | +### When to use Lockstep |
| 89 | + |
| 90 | +Lockstep is useful when: |
| 91 | + |
| 92 | +- **Testing** — deterministic event ordering makes assertions straightforward. |
| 93 | +- **Debugging** — step-through debugging is easier when execution stays on the consumer's thread. |
| 94 | +- **Ordered processing** — scenarios where you need to fully process one superstep's events before the next superstep begins. |
| 95 | + |
| 96 | +## Choosing an Execution Mode |
| 97 | + |
| 98 | +For most production scenarios, the default **OffThread** mode is recommended. It provides the best responsiveness and allows the workflow to continue processing while the consumer handles events. |
| 99 | + |
| 100 | +Use **Lockstep** when deterministic behavior is more important than performance, such as in unit tests or debugging sessions. |
| 101 | + |
| 102 | +```csharp |
| 103 | +// Production: OffThread (default) |
| 104 | +await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, input); |
| 105 | + |
| 106 | +// Testing: Lockstep for deterministic behavior |
| 107 | +await using StreamingRun run = await InProcessExecution.Lockstep.RunStreamingAsync(workflow, input); |
| 108 | +``` |
| 109 | + |
| 110 | +## Non-Streaming Execution |
| 111 | + |
| 112 | +Both execution modes support non-streaming execution via `RunAsync`. In non-streaming mode, the workflow runs to completion and collects all events into a `Run` object rather than streaming them incrementally: |
| 113 | + |
| 114 | +```csharp |
| 115 | +Run run = await InProcessExecution.RunAsync(workflow, input); |
| 116 | + |
| 117 | +// Access all emitted events |
| 118 | +foreach (WorkflowEvent evt in run.OutgoingEvents) |
| 119 | +{ |
| 120 | + // Process events |
| 121 | +} |
| 122 | +``` |
| 123 | + |
| 124 | +Because non-streaming execution collects all events after completion, the real-time event delivery benefit of OffThread does not apply. The primary difference between modes in non-streaming scenarios is **threading**: OffThread runs supersteps on a background thread, freeing the calling thread while awaiting completion, whereas Lockstep runs supersteps on the caller's thread, blocking it until the workflow finishes. |
| 125 | + |
| 126 | +Non-streaming execution uses the default OffThread mode. To use Lockstep with non-streaming execution: |
| 127 | + |
| 128 | +```csharp |
| 129 | +Run run = await InProcessExecution.Lockstep.RunAsync(workflow, input); |
| 130 | +``` |
| 131 | + |
| 132 | +## Next steps |
| 133 | + |
| 134 | +> [!div class="nextstepaction"] |
| 135 | +> [Workflow Builder & Execution](../workflows.md) |
| 136 | +
|
| 137 | +::: zone-end |
| 138 | + |
| 139 | +::: zone pivot="programming-language-python" |
| 140 | + |
| 141 | +Execution modes are not applicable to Python workflows. Python workflows use a single execution model that handles superstep processing and event delivery through an asynchronous generator. This model is similar to the .NET Lockstep mode — steps don't advance unless the consumer is actively pulling events from the generator. |
| 142 | + |
| 143 | +For information on running Python workflows, see [Workflow Builder & Execution](../workflows.md). |
| 144 | + |
| 145 | +::: zone-end |
0 commit comments