diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index de8aef42fc..86eee2fbbe 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -143,6 +143,7 @@
+
diff --git a/dotnet/samples/GettingStarted/Workflows/README.md b/dotnet/samples/GettingStarted/Workflows/README.md
index 4ea750e19e..072acfa560 100644
--- a/dotnet/samples/GettingStarted/Workflows/README.md
+++ b/dotnet/samples/GettingStarted/Workflows/README.md
@@ -19,6 +19,7 @@ Please begin with the [Foundational](./_Foundational) samples in order. These th
| [Multi-Service Workflows](./_Foundational/05_MultiModelService) | Shows using multiple AI services in the same workflow |
| [Sub-Workflows](./_Foundational/06_SubWorkflows) | Demonstrates composing workflows hierarchically by embedding workflows as executors |
| [Mixed Workflow with Agents and Executors](./_Foundational/07_MixedWorkflowAgentsAndExecutors) | Shows how to mix agents and executors with adapter pattern for type conversion and protocol handling |
+| [Writer-Critic Workflow](./_Foundational/08_WriterCriticWorkflow) | Demonstrates iterative refinement with quality gates, max iteration safety, multiple message handlers, and conditional routing for feedback loops |
Once completed, please proceed to other samples listed below.
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/08_WriterCriticWorkflow.csproj b/dotnet/samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/08_WriterCriticWorkflow.csproj
new file mode 100644
index 0000000000..3e8f2547d1
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/08_WriterCriticWorkflow.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ net9.0
+ WriterCriticWorkflow
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/Program.cs b/dotnet/samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/Program.cs
new file mode 100644
index 0000000000..fc39044b42
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/Program.cs
@@ -0,0 +1,409 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.Extensions.AI;
+
+namespace WriterCriticWorkflow;
+
+///
+/// This sample demonstrates an iterative refinement workflow between Writer and Critic agents.
+///
+/// The workflow implements a content creation and review loop that:
+/// 1. Writer creates initial content based on the user's request
+/// 2. Critic reviews the content and provides feedback using structured output
+/// 3. If approved: Summary executor presents the final content
+/// 4. If rejected: Writer revises based on feedback (loops back)
+/// 5. Continues until approval or max iterations (3) is reached
+///
+/// This pattern is useful when you need:
+/// - Iterative content improvement through feedback loops
+/// - Quality gates with reviewer approval
+/// - Maximum iteration limits to prevent infinite loops
+/// - Conditional workflow routing based on agent decisions
+/// - Structured output for reliable decision-making
+///
+/// Key Learning: Workflows can implement loops with conditional edges, shared state,
+/// and structured output for robust agent decision-making.
+///
+///
+/// Pre-requisites:
+/// - Previous foundational samples should be completed first.
+/// - An Azure OpenAI chat completion deployment must be configured.
+///
+public static class Program
+{
+ public const int MaxIterations = 3;
+
+ private static async Task Main()
+ {
+ Console.WriteLine("\n=== Writer-Critic Iteration Workflow ===\n");
+ Console.WriteLine($"Writer and Critic will iterate up to {MaxIterations} times until approval.\n");
+
+ // Set up the Azure OpenAI client
+ string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+ string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
+ IChatClient chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()).GetChatClient(deploymentName).AsIChatClient();
+
+ // Create executors for content creation and review
+ WriterExecutor writer = new(chatClient);
+ CriticExecutor critic = new(chatClient);
+ SummaryExecutor summary = new(chatClient);
+
+ // Build the workflow with conditional routing based on critic's decision
+ WorkflowBuilder workflowBuilder = new WorkflowBuilder(writer)
+ .AddEdge(writer, critic)
+ .AddSwitch(critic, sw => sw
+ .AddCase(cd => cd?.Approved == true, summary)
+ .AddCase(cd => cd?.Approved == false, writer))
+ .WithOutputFrom(summary);
+
+ // Execute the workflow with a sample task
+ // The workflow loops back to Writer if content is rejected,
+ // or proceeds to Summary if approved. State tracking ensures we don't loop forever.
+ Console.WriteLine(new string('=', 80));
+ Console.WriteLine("TASK: Write a short blog post about AI ethics (200 words)");
+ Console.WriteLine(new string('=', 80) + "\n");
+
+ const string InitialTask = "Write a 200-word blog post about AI ethics. Make it thoughtful and engaging.";
+
+ Workflow workflow = workflowBuilder.Build();
+ await ExecuteWorkflowAsync(workflow, InitialTask);
+
+ Console.WriteLine("\n✅ Sample Complete: Writer-Critic iteration demonstrates conditional workflow loops\n");
+ Console.WriteLine("Key Concepts Demonstrated:");
+ Console.WriteLine(" ✓ Iterative refinement loop with conditional routing");
+ Console.WriteLine(" ✓ Shared workflow state for iteration tracking");
+ Console.WriteLine($" ✓ Max iteration cap ({MaxIterations}) for safety");
+ Console.WriteLine(" ✓ Multiple message handlers in a single executor");
+ Console.WriteLine(" ✓ Streaming support with structured output\n");
+ }
+
+ private static async Task ExecuteWorkflowAsync(Workflow workflow, string input)
+ {
+ // Execute in streaming mode to see real-time progress
+ await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, input);
+
+ // Watch the workflow events
+ await foreach (WorkflowEvent evt in run.WatchStreamAsync())
+ {
+ switch (evt)
+ {
+ case AgentRunUpdateEvent agentUpdate:
+ // Stream agent output in real-time
+ if (!string.IsNullOrEmpty(agentUpdate.Update.Text))
+ {
+ Console.Write(agentUpdate.Update.Text);
+ }
+ break;
+
+ case WorkflowOutputEvent output:
+ Console.WriteLine("\n\n" + new string('=', 80));
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.WriteLine("✅ FINAL APPROVED CONTENT");
+ Console.ResetColor();
+ Console.WriteLine(new string('=', 80));
+ Console.WriteLine();
+ Console.WriteLine(output.Data);
+ Console.WriteLine();
+ Console.WriteLine(new string('=', 80));
+ break;
+ }
+ }
+ }
+}
+
+// ====================================
+// Shared State for Iteration Tracking
+// ====================================
+
+///
+/// Tracks the current iteration and conversation history across workflow executions.
+///
+internal sealed class FlowState
+{
+ public int Iteration { get; set; } = 1;
+ public List History { get; } = [];
+}
+
+///
+/// Constants for accessing the shared flow state in workflow context.
+///
+internal static class FlowStateShared
+{
+ public const string Scope = "FlowStateScope";
+ public const string Key = "singleton";
+}
+
+///
+/// Helper methods for reading and writing shared flow state.
+///
+internal static class FlowStateHelpers
+{
+ public static async Task ReadFlowStateAsync(IWorkflowContext context)
+ {
+ FlowState? state = await context.ReadStateAsync(FlowStateShared.Key, scopeName: FlowStateShared.Scope);
+ return state ?? new FlowState();
+ }
+
+ public static ValueTask SaveFlowStateAsync(IWorkflowContext context, FlowState state)
+ => context.QueueStateUpdateAsync(FlowStateShared.Key, state, scopeName: FlowStateShared.Scope);
+}
+
+// ====================================
+// Data Transfer Objects
+// ====================================
+
+///
+/// Structured output schema for the Critic's decision.
+/// Uses JsonPropertyName and Description attributes for OpenAI's JSON schema.
+///
+[Description("Critic's review decision including approval status and feedback")]
+[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated via JSON deserialization")]
+internal sealed class CriticDecision
+{
+ [JsonPropertyName("approved")]
+ [Description("Whether the content is approved (true) or needs revision (false)")]
+ public bool Approved { get; set; }
+
+ [JsonPropertyName("feedback")]
+ [Description("Specific feedback for improvements if not approved, empty if approved")]
+ public string Feedback { get; set; } = "";
+
+ // Non-JSON properties for workflow use
+ [JsonIgnore]
+ public string Content { get; set; } = "";
+
+ [JsonIgnore]
+ public int Iteration { get; set; }
+}
+
+// ====================================
+// Custom Executors
+// ====================================
+
+///
+/// Executor that creates or revises content based on user requests or critic feedback.
+/// This executor demonstrates multiple message handlers for different input types.
+///
+internal sealed class WriterExecutor : Executor
+{
+ private readonly AIAgent _agent;
+
+ public WriterExecutor(IChatClient chatClient) : base("Writer")
+ {
+ this._agent = new ChatClientAgent(
+ chatClient,
+ name: "Writer",
+ instructions: """
+ You are a skilled writer. Create clear, engaging content.
+ If you receive feedback, carefully revise the content to address all concerns.
+ Maintain the same topic and length requirements.
+ """
+ );
+ }
+
+ protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
+ routeBuilder
+ .AddHandler(this.HandleInitialRequestAsync)
+ .AddHandler(this.HandleRevisionRequestAsync);
+
+ ///
+ /// Handles the initial writing request from the user.
+ ///
+ private async ValueTask HandleInitialRequestAsync(
+ string message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ return await this.HandleAsyncCoreAsync(new ChatMessage(ChatRole.User, message), context, cancellationToken);
+ }
+
+ ///
+ /// Handles revision requests from the critic with feedback.
+ ///
+ private async ValueTask HandleRevisionRequestAsync(
+ CriticDecision decision,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ string prompt = "Revise the following content based on this feedback:\n\n" +
+ $"Feedback: {decision.Feedback}\n\n" +
+ $"Original Content:\n{decision.Content}";
+
+ return await this.HandleAsyncCoreAsync(new ChatMessage(ChatRole.User, prompt), context, cancellationToken);
+ }
+
+ ///
+ /// Core implementation for generating content (initial or revised).
+ ///
+ private async Task HandleAsyncCoreAsync(
+ ChatMessage message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken)
+ {
+ FlowState state = await FlowStateHelpers.ReadFlowStateAsync(context);
+
+ Console.WriteLine($"\n=== Writer (Iteration {state.Iteration}) ===\n");
+
+ StringBuilder sb = new();
+ await foreach (AgentRunResponseUpdate update in this._agent.RunStreamingAsync(message, cancellationToken: cancellationToken))
+ {
+ if (!string.IsNullOrEmpty(update.Text))
+ {
+ sb.Append(update.Text);
+ Console.Write(update.Text);
+ }
+ }
+ Console.WriteLine("\n");
+
+ string text = sb.ToString();
+ state.History.Add(new ChatMessage(ChatRole.Assistant, text));
+ await FlowStateHelpers.SaveFlowStateAsync(context, state);
+
+ return new ChatMessage(ChatRole.User, text);
+ }
+}
+
+///
+/// Executor that reviews content and decides whether to approve or request revisions.
+/// Uses structured output with streaming for reliable decision-making.
+///
+internal sealed class CriticExecutor : Executor
+{
+ private readonly AIAgent _agent;
+
+ public CriticExecutor(IChatClient chatClient) : base("Critic")
+ {
+ this._agent = new ChatClientAgent(chatClient, new ChatClientAgentOptions
+ {
+ Name = "Critic",
+ Instructions = """
+ You are a constructive critic. Review the content and provide specific feedback.
+ Always try to provide actionable suggestions for improvement and strive to identify improvement points.
+ Only approve if the content is high quality, clear, and meets the original requirements and you see no improvement points.
+
+ Provide your decision as structured output with:
+ - approved: true if content is good, false if revisions needed
+ - feedback: specific improvements needed (empty if approved)
+
+ Be concise but specific in your feedback.
+ """,
+ ChatOptions = new()
+ {
+ ResponseFormat = ChatResponseFormat.ForJsonSchema()
+ }
+ });
+ }
+
+ public override async ValueTask HandleAsync(
+ ChatMessage message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ FlowState state = await FlowStateHelpers.ReadFlowStateAsync(context);
+
+ Console.WriteLine($"=== Critic (Iteration {state.Iteration}) ===\n");
+
+ // Use RunStreamingAsync to get streaming updates, then deserialize at the end
+ IAsyncEnumerable updates = this._agent.RunStreamingAsync(message, cancellationToken: cancellationToken);
+
+ // Stream the output in real-time (for any rationale/explanation)
+ await foreach (AgentRunResponseUpdate update in updates)
+ {
+ if (!string.IsNullOrEmpty(update.Text))
+ {
+ Console.Write(update.Text);
+ }
+ }
+ Console.WriteLine("\n");
+
+ // Convert the stream to a response and deserialize the structured output
+ AgentRunResponse response = await updates.ToAgentRunResponseAsync(cancellationToken);
+ CriticDecision decision = response.Deserialize(JsonSerializerOptions.Web);
+
+ Console.WriteLine($"Decision: {(decision.Approved ? "✅ APPROVED" : "❌ NEEDS REVISION")}");
+ if (!string.IsNullOrEmpty(decision.Feedback))
+ {
+ Console.WriteLine($"Feedback: {decision.Feedback}");
+ }
+ Console.WriteLine();
+
+ // Safety: approve if max iterations reached
+ if (!decision.Approved && state.Iteration >= Program.MaxIterations)
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($"⚠️ Max iterations ({Program.MaxIterations}) reached - auto-approving");
+ Console.ResetColor();
+ decision.Approved = true;
+ decision.Feedback = "";
+ }
+
+ // Increment iteration ONLY if rejecting (will loop back to Writer)
+ if (!decision.Approved)
+ {
+ state.Iteration++;
+ }
+
+ // Store the decision in history
+ state.History.Add(new ChatMessage(ChatRole.Assistant,
+ $"[Decision: {(decision.Approved ? "Approved" : "Needs Revision")}] {decision.Feedback}"));
+ await FlowStateHelpers.SaveFlowStateAsync(context, state);
+
+ // Populate workflow-specific fields
+ decision.Content = message.Text ?? "";
+ decision.Iteration = state.Iteration;
+
+ return decision;
+ }
+}
+
+///
+/// Executor that presents the final approved content to the user.
+///
+internal sealed class SummaryExecutor : Executor
+{
+ private readonly AIAgent _agent;
+
+ public SummaryExecutor(IChatClient chatClient) : base("Summary")
+ {
+ this._agent = new ChatClientAgent(
+ chatClient,
+ name: "Summary",
+ instructions: """
+ You present the final approved content to the user.
+ Simply output the polished content - no additional commentary needed.
+ """
+ );
+ }
+
+ public override async ValueTask HandleAsync(
+ CriticDecision message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine("=== Summary ===\n");
+
+ string prompt = $"Present this approved content:\n\n{message.Content}";
+
+ StringBuilder sb = new();
+ await foreach (AgentRunResponseUpdate update in this._agent.RunStreamingAsync(new ChatMessage(ChatRole.User, prompt), cancellationToken: cancellationToken))
+ {
+ if (!string.IsNullOrEmpty(update.Text))
+ {
+ sb.Append(update.Text);
+ }
+ }
+
+ ChatMessage result = new(ChatRole.Assistant, sb.ToString());
+ await context.YieldOutputAsync(result, cancellationToken);
+ return result;
+ }
+}