Skip to content

Commit a1c1e5b

Browse files
authored
document workflow IResettableExecutor
* document workflow IResettableExecutor * Update python zone pivots. * Removed unnecessary comment.
1 parent 4d7a345 commit a1c1e5b

4 files changed

Lines changed: 156 additions & 9 deletions

File tree

agent-framework/TOC.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ items:
158158
items:
159159
- name: Agent Executor
160160
href: workflows/advanced/agent-executor.md
161+
- name: Resettable Executors
162+
href: workflows/advanced/resettable-executors.md
161163
- name: Integrations
162164
items:
163165
- name: Overview
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
---
2+
title: Resettable Executors
3+
description: How to implement IResettableExecutor to safely reuse stateful executors across workflow runs.
4+
zone_pivot_groups: programming-languages
5+
author: peibekwe
6+
ms.topic: conceptual
7+
ms.author: peibekwe
8+
ms.date: 03/25/2026
9+
ms.service: agent-framework
10+
---
11+
12+
13+
# Resettable Executors
14+
15+
::: zone pivot="programming-language-csharp"
16+
17+
## Overview
18+
19+
Executors in workflows are often stateful — for example, they may accumulate messages, track turn counts, or cache intermediate results. When a workflow is reused across multiple runs with shared executor instances, leftover state from a previous run can leak into subsequent runs, causing unexpected behavior or data corruption.
20+
21+
The `IResettableExecutor` interface solves this by providing a contract for executors to clear their internal state between runs. The workflow runtime automatically calls `ResetAsync()` on shared executor instances when a run completes, ensuring a clean slate for the next run.
22+
23+
## The Problem
24+
25+
Consider an executor that collects messages during a workflow run:
26+
27+
```csharp
28+
internal sealed partial class AggregationExecutor() : Executor("AggregationExecutor")
29+
{
30+
private readonly List<string> _messages = [];
31+
32+
[MessageHandler]
33+
private async ValueTask HandleAsync(string message, IWorkflowContext context)
34+
{
35+
this._messages.Add(message);
36+
// Process aggregated messages...
37+
}
38+
}
39+
```
40+
41+
If this executor is shared across workflow runs, `_messages` retains data from the previous run. The second run would see stale messages that don't belong to it.
42+
43+
## The IResettableExecutor Interface
44+
45+
`IResettableExecutor` defines a single method that the workflow runtime calls between runs:
46+
47+
```csharp
48+
public interface IResettableExecutor
49+
{
50+
ValueTask ResetAsync();
51+
}
52+
```
53+
54+
When an executor implements this interface, the runtime can safely reset it after each run, allowing the workflow to be reused without stale state.
55+
56+
## Implementing IResettableExecutor
57+
58+
To make a stateful executor resettable, implement the interface and clear all mutable state in `ResetAsync()`:
59+
60+
```csharp
61+
internal sealed partial class AggregationExecutor()
62+
: Executor("AggregationExecutor"), IResettableExecutor
63+
{
64+
private readonly List<string> _messages = [];
65+
66+
[MessageHandler]
67+
private async ValueTask HandleAsync(string message, IWorkflowContext context)
68+
{
69+
this._messages.Add(message);
70+
// Process aggregated messages...
71+
}
72+
73+
public ValueTask ResetAsync()
74+
{
75+
this._messages.Clear();
76+
return default;
77+
}
78+
}
79+
```
80+
81+
For a complete working example of a workflow that uses resettable executors, see the [WorkflowAsAnAgent sample](https://github.com/microsoft/agent-framework/tree/main/dotnet/samples/03-workflows/Agents/WorkflowAsAnAgent).
82+
83+
## When to Implement
84+
85+
Not all executors need to implement `IResettableExecutor`. Use this decision guide:
86+
87+
| Scenario | Implement? | Reason |
88+
|----------|:----------:|--------|
89+
| Executor has mutable state (lists, counters, caches) and is shared across runs | **Yes** | State from one run would leak into the next |
90+
| Executor is stateless | No | Nothing to reset |
91+
| Executor is created fresh per workflow (via a factory method) | No | Each run gets a new instance with clean state |
92+
| Executor is declared as cross-run shareable (`declareCrossRunShareable: true`) | No | Cross-run shareable executors support concurrent use without resetting |
93+
94+
> [!WARNING]
95+
> If a shared stateful executor does not implement `IResettableExecutor`, reusing the workflow throws an `InvalidOperationException`:
96+
>
97+
> `"Cannot reuse Workflow with shared Executor instances that do not implement IResettableExecutor."`
98+
99+
## How the Runtime Uses It
100+
101+
The workflow runtime manages the reset lifecycle automatically. You do not need to call `ResetAsync()` yourself. The sequence is:
102+
103+
1. **Ownership acquired** — when a workflow run starts, the runtime takes ownership of the workflow instance and notes which executors need resetting.
104+
2. **Run executes** — executors process messages and may accumulate state.
105+
3. **Ownership released** — when the run completes (or is disposed), the runtime releases ownership and calls `ResetAsync()` on all shared executor instances that implement `IResettableExecutor`.
106+
4. **Ready for reuse** — after a successful reset, the workflow can be used for a new run.
107+
108+
If any shared executor fails to reset (because it does not implement the interface), the workflow is marked as non-reusable and subsequent runs will throw.
109+
110+
## Relationship to State Isolation
111+
112+
`IResettableExecutor` complements the helper-method pattern described in [State Management](../state.md). The two approaches serve different needs:
113+
114+
- **Helper methods** (creating fresh instances per run) provide the strongest isolation guarantees and are recommended as the default approach.
115+
- **`IResettableExecutor`** is useful when you need to share executor instances across runs — for example, when executor construction is expensive or when a workflow is exposed as an agent and reused across multiple invocations.
116+
117+
Choose the approach that best fits your scenario. For most workflows, helper methods are sufficient. Use `IResettableExecutor` when sharing instances is a deliberate design choice.
118+
119+
::: zone-end
120+
121+
::: zone pivot="programming-language-python"
122+
123+
This concept does not apply to Python. For full state isolation, build fresh workflow and executor instances for each independent run. See [State Management](../state.md) for patterns and examples.
124+
125+
::: zone-end
126+
127+
## Next steps
128+
129+
> [!div class="nextstepaction"]
130+
> [State Management](../state.md)

agent-framework/workflows/executors.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,21 @@ zone_pivot_groups: programming-languages
55
author: TaoChenOSU
66
ms.topic: conceptual
77
ms.author: taochen
8-
ms.date: 03/05/2026
8+
ms.date: 03/24/2026
99
ms.service: agent-framework
1010
---
1111

1212
<!--
1313
Language parity table – keep in sync when adding/removing sections.
1414
15-
| Section | C# | Python | |
16-
|----------------------------|:--:|:-------:|-----------------|
17-
| Basic Executor Structure | ✅ | ✅ | |
18-
| Multiple Input Types | ✅ | ✅ | |
19-
| Function-Based Executors | ✅ | ✅ | |
20-
| Explicit Type Parameters | ❌ | ✅ | Python-specific |
21-
| The WorkflowContext Object | ✅ | ✅ | |
15+
| Section | C# | Python | Notes |
16+
|----------------------------|:--:|:------:|------------------|
17+
| Basic Executor Structure | ✅ | ✅ | |
18+
| Resettable Executors (TIP) | ✅ | ❌ | C#-specific; links to advanced page |
19+
| Multiple Input Types | ✅ | ✅ | |
20+
| Function-Based Executors | ✅ | ✅ | |
21+
| Explicit Type Parameters | ❌ | ✅ | Python-specific |
22+
| The WorkflowContext Object | ✅ | ✅ | |
2223
-->
2324

2425
# Executors
@@ -69,6 +70,9 @@ internal sealed partial class UppercaseExecutor() : Executor("UppercaseExecutor"
6970
}
7071
```
7172

73+
> [!TIP]
74+
> Executors can hold mutable state. If a stateful executor is shared across workflow runs, it must implement `IResettableExecutor` to clear stale state between runs. See [Resettable Executors](./advanced/resettable-executors.md) for details.
75+
7276
## Multiple Input Types
7377

7478
Handle multiple input types by defining multiple `[MessageHandler]` methods:

agent-framework/workflows/state.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ zone_pivot_groups: programming-languages
55
author: TaoChenOSU
66
ms.topic: conceptual
77
ms.author: taochen
8-
ms.date: 03/09/2026
8+
ms.date: 03/25/2026
99
ms.service: agent-framework
1010
---
1111

@@ -18,6 +18,7 @@ ms.service: agent-framework
1818
| Accessing State | ✅ | ✅ | |
1919
| State Isolation – Mutable vs Immutable | ✅ | ✅ | Prose only, no code needed |
2020
| State Isolation – Helper Methods | ❌ | ✅ | C# coming soon |
21+
| State Isolation – Resetting Shared Executors| ✅ | ❌ | C#-specific; links to advanced page |
2122
| Agent State Management | ❌ | ✅ | C# coming soon |
2223
-->
2324

@@ -191,6 +192,16 @@ workflow_b = create_workflow()
191192
> [!TIP]
192193
> To ensure proper state isolation and thread safety, also make sure that executor instances created inside the helper method do not share external mutable state.
193194
195+
::: zone pivot="programming-language-csharp"
196+
197+
### Resetting Shared Executors
198+
199+
If you need to share executor instances across workflow runs — for example, when executor construction is expensive or when a workflow is exposed as an agent — stateful executors must implement `IResettableExecutor`. This interface provides a `ResetAsync()` method that the workflow runtime calls automatically between runs to clear stale state.
200+
201+
For details on when and how to implement `IResettableExecutor`, see [Resettable Executors](./advanced/resettable-executors.md).
202+
203+
::: zone-end
204+
194205
### Agent State Management
195206

196207
Agent context is managed via agent threads. By default, each agent in a workflow will get its own thread unless the agent is managed by a custom executor. For more information, refer to [Working with Agents](./agents-in-workflows.md).

0 commit comments

Comments
 (0)