diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 246b3e7e7b..7b6327c13e 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -55,6 +55,7 @@
+
diff --git a/dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowLoop/08_WorkflowLoop.csproj b/dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowLoop/08_WorkflowLoop.csproj
new file mode 100644
index 0000000000..8a0c04e2dc
--- /dev/null
+++ b/dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowLoop/08_WorkflowLoop.csproj
@@ -0,0 +1,30 @@
+
+
+ net10.0
+ Exe
+ enable
+ enable
+ WorkflowLoop
+ WorkflowLoop
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowLoop/FeedbackExecutor.cs b/dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowLoop/FeedbackExecutor.cs
new file mode 100644
index 0000000000..b212af1015
--- /dev/null
+++ b/dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowLoop/FeedbackExecutor.cs
@@ -0,0 +1,88 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.Extensions.AI;
+
+namespace WorkflowLoop;
+
+///
+/// Evaluates slogans and either accepts them or sends feedback back for refinement.
+/// Uses SendMessageAsync to loop back to SloganWriter and YieldOutputAsync to end the workflow.
+///
+internal sealed class FeedbackExecutor : Executor
+{
+ private readonly AIAgent _agent;
+ private AgentSession? _session;
+
+ ///
+ /// Gets or sets the minimum rating required to accept a slogan.
+ ///
+ public int MinimumRating { get; init; } = 9;
+
+ ///
+ /// Gets or sets the maximum number of refinement attempts before accepting the slogan.
+ ///
+ public int MaxAttempts { get; init; } = 3;
+
+ private int _attempts;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A unique identifier for the executor.
+ /// The chat client to use for the AI agent.
+ public FeedbackExecutor(string id, IChatClient chatClient) : base(id)
+ {
+ ChatClientAgentOptions agentOptions = new()
+ {
+ ChatOptions = new()
+ {
+ Instructions = "You are a professional editor. You will be given a slogan and the task it is meant to accomplish.",
+ ResponseFormat = ChatResponseFormat.ForJsonSchema()
+ }
+ };
+
+ this._agent = new ChatClientAgent(chatClient, agentOptions);
+ }
+
+ ///
+ public override async ValueTask HandleAsync(SloganResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine($" [FeedbackProvider] Evaluating slogan: \"{message.Slogan}\"");
+ this._session ??= await this._agent.GetNewSessionAsync(cancellationToken);
+
+ string sloganMessage = $"""
+ Here is a slogan for the task '{message.Task}':
+ Slogan: {message.Slogan}
+ Please provide feedback on this slogan, including comments, a rating from 1 to 10, and suggested actions for improvement.
+ """;
+
+ AgentResponse response = await this._agent.RunAsync(sloganMessage, this._session, cancellationToken: cancellationToken);
+ FeedbackResult feedback = JsonSerializer.Deserialize(response.Text) ?? throw new InvalidOperationException("Failed to deserialize feedback.");
+
+ Console.WriteLine($" [FeedbackProvider] Rating: {feedback.Rating}/{this.MinimumRating} - {feedback.Comments}");
+
+ // If the rating meets the threshold, accept the slogan and end the workflow
+ if (feedback.Rating >= this.MinimumRating)
+ {
+ Console.WriteLine(" [FeedbackProvider] Accepted!");
+ await context.YieldOutputAsync($"The following slogan was accepted:\n\n{message.Slogan}", cancellationToken);
+ return;
+ }
+
+ // If we've exceeded max attempts, accept the slogan anyway
+ if (this._attempts >= this.MaxAttempts)
+ {
+ Console.WriteLine(" [FeedbackProvider] Max attempts reached, accepting final slogan.");
+ await context.YieldOutputAsync($"The slogan was accepted after {this.MaxAttempts} attempts. Final slogan:\n\n{message.Slogan}", cancellationToken);
+ return;
+ }
+
+ // Otherwise, send feedback back to the slogan writer for refinement (circular edge)
+ Console.WriteLine($" [FeedbackProvider] Sending back for refinement (attempt {this._attempts + 1}/{this.MaxAttempts})...");
+ await context.SendMessageAsync(feedback, cancellationToken: cancellationToken);
+ this._attempts++;
+ }
+}
diff --git a/dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowLoop/Program.cs b/dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowLoop/Program.cs
new file mode 100644
index 0000000000..fc83baf150
--- /dev/null
+++ b/dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowLoop/Program.cs
@@ -0,0 +1,91 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates a CYCLIC WORKFLOW (back-edges in the graph).
+// SloganWriter and FeedbackProvider loop until the slogan meets quality criteria.
+
+using Azure;
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI.DurableTask;
+using Microsoft.Agents.AI.DurableTask.Workflows;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.DurableTask.Client.AzureManaged;
+using Microsoft.DurableTask.Worker.AzureManaged;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using WorkflowLoop;
+
+// Get DTS connection string from environment variable
+string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
+ ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
+
+string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
+ ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
+ ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
+string? azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
+
+// Create the chat client using key-based or Azure CLI credential authentication
+AzureOpenAIClient openAiClient = !string.IsNullOrEmpty(azureOpenAiKey)
+ ? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
+ : new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
+IChatClient chatClient = openAiClient.GetChatClient(deploymentName).AsIChatClient();
+
+// Define executors for the workflow
+SloganWriterExecutor sloganWriter = new("SloganWriter", chatClient);
+FeedbackExecutor feedbackProvider = new("FeedbackProvider", chatClient);
+
+// Build the workflow with a circular edge: SloganWriter → FeedbackProvider → SloganWriter
+Workflow workflow = new WorkflowBuilder(sloganWriter)
+ .WithName("SloganCreationWorkflow")
+ .AddEdge(sloganWriter, feedbackProvider)
+ .AddEdge(feedbackProvider, sloganWriter)
+ .WithOutputFrom(feedbackProvider)
+ .Build();
+
+IHost host = Host.CreateDefaultBuilder(args)
+ .ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
+ .ConfigureServices(services =>
+ {
+ services.ConfigureDurableWorkflows(
+ workflowOptions => workflowOptions.AddWorkflow(workflow),
+ workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
+ clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
+ })
+ .Build();
+
+await host.StartAsync();
+
+IWorkflowClient workflowClient = host.Services.GetRequiredService();
+
+Console.WriteLine("Workflow Loop Demo - Enter a topic for slogan generation (or 'exit'):");
+
+while (true)
+{
+ Console.Write("> ");
+ string? input = Console.ReadLine();
+ if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
+ {
+ break;
+ }
+
+ try
+ {
+ // RunAsync starts the workflow; cast to IAwaitableWorkflowRun to wait for the result
+ IAwaitableWorkflowRun run = (IAwaitableWorkflowRun)await workflowClient.RunAsync(workflow, input);
+ Console.WriteLine($"Started run: {run.RunId}");
+
+ string? result = await run.WaitForCompletionAsync();
+ Console.WriteLine($"Result: {result}");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error: {ex.Message}");
+ }
+
+ Console.WriteLine();
+}
+
+await host.StopAsync();
diff --git a/dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowLoop/README.md b/dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowLoop/README.md
new file mode 100644
index 0000000000..2d57c89e91
--- /dev/null
+++ b/dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowLoop/README.md
@@ -0,0 +1,69 @@
+# Workflow Loop Sample
+
+This sample demonstrates how to run a **cyclic workflow** (containing loops / back-edges) as a durable orchestration. The workflow iteratively improves a slogan based on AI feedback until it meets quality criteria.
+
+## Key Concepts Demonstrated
+
+- **Cyclic workflow support** — back-edges in the graph (A → B → A)
+- **Multi-type executor handlers** — SloganWriter handles both `string` and `FeedbackResult` inputs
+- **Message routing via `SendMessageAsync`** — FeedbackProvider sends messages back to SloganWriter
+- **Workflow termination via `YieldOutputAsync`** — FeedbackProvider yields output when the slogan is accepted
+
+## Overview
+
+```
+ ┌──────────────────────┐
+ │ │
+ input ──→ SloganWriter ──→ FeedbackProvider
+ ▲ │
+ │ (FeedbackResult) │
+ └──────────────────────┘
+ back-edge
+```
+
+| Executor | Description |
+|----------|-------------|
+| SloganWriter | Generates slogans from user input; refines them based on feedback |
+| FeedbackProvider | Evaluates slogans — accepts (YieldOutput) or loops (SendMessage) |
+
+### Loop Behavior
+
+1. **SloganWriter** generates a slogan based on user input
+2. **FeedbackProvider** evaluates the slogan and provides a rating
+3. If the rating is below the threshold (default: 9), feedback is sent back to SloganWriter
+4. SloganWriter improves the slogan based on feedback
+5. The loop continues until the slogan is accepted or max attempts (default: 3) are reached
+
+## Environment Setup
+
+See the [README.md](../README.md) file in the parent directory for information on configuring the environment, including how to install and run the Durable Task Scheduler.
+
+### Required Environment Variables
+
+| Variable | Description |
+|----------|-------------|
+| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI endpoint URL |
+| `AZURE_OPENAI_DEPLOYMENT` | Azure OpenAI deployment name |
+| `AZURE_OPENAI_KEY` | (Optional) Azure OpenAI API key. If not set, uses Azure CLI credential |
+| `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` | (Optional) DTS connection string. Defaults to local emulator |
+
+## Running the Sample
+
+```bash
+cd dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowLoop
+dotnet run --framework net10.0
+```
+
+### Sample Output
+
+```text
+Workflow Loop Demo - Enter a topic for slogan generation (or 'exit'):
+> sustainable energy
+Started run: abc123...
+ [FeedbackProvider] Rating 6/9 - sending back for refinement (attempt 1/3)
+ [FeedbackProvider] Rating 8/9 - sending back for refinement (attempt 2/3)
+ Event: WorkflowOutputEvent
+ Completed: The following slogan was accepted:
+
+Power the future, preserve the planet.
+```
diff --git a/dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowLoop/SloganResult.cs b/dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowLoop/SloganResult.cs
new file mode 100644
index 0000000000..4b1f0befa5
--- /dev/null
+++ b/dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowLoop/SloganResult.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace WorkflowLoop;
+
+///
+/// Represents the result produced by the slogan writer executor.
+///
+public sealed class SloganResult
+{
+ [JsonPropertyName("task")]
+ public required string Task { get; set; }
+
+ [JsonPropertyName("slogan")]
+ public required string Slogan { get; set; }
+}
+
+///
+/// Represents feedback from the feedback executor, including comments, rating, and improvement actions.
+///
+public sealed class FeedbackResult
+{
+ [JsonPropertyName("comments")]
+ public string Comments { get; set; } = string.Empty;
+
+ [JsonPropertyName("rating")]
+ public int Rating { get; set; }
+
+ [JsonPropertyName("actions")]
+ public string Actions { get; set; } = string.Empty;
+}
diff --git a/dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowLoop/SloganWriterExecutor.cs b/dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowLoop/SloganWriterExecutor.cs
new file mode 100644
index 0000000000..f609d346ee
--- /dev/null
+++ b/dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowLoop/SloganWriterExecutor.cs
@@ -0,0 +1,80 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.Extensions.AI;
+
+namespace WorkflowLoop;
+
+///
+/// Generates slogans based on user input or refines them based on feedback.
+/// This executor handles two input types: string (initial request) and FeedbackResult (refinement loop).
+///
+internal sealed class SloganWriterExecutor : Executor
+{
+ private readonly AIAgent _agent;
+ private AgentSession? _session;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A unique identifier for the executor.
+ /// The chat client to use for the AI agent.
+ public SloganWriterExecutor(string id, IChatClient chatClient) : base(id)
+ {
+ ChatClientAgentOptions agentOptions = new()
+ {
+ ChatOptions = new()
+ {
+ Instructions = "You are a professional slogan writer. You will be given a task to create a slogan.",
+ ResponseFormat = ChatResponseFormat.ForJsonSchema()
+ }
+ };
+
+ this._agent = new ChatClientAgent(chatClient, agentOptions);
+ }
+
+ ///
+ /// Configures two routes: one for initial string input and one for feedback-based refinement.
+ ///
+ protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
+ routeBuilder.AddHandler(this.HandleAsync)
+ .AddHandler(this.HandleFeedbackAsync);
+
+ ///
+ /// Handles the initial slogan generation request.
+ ///
+ public async ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine($" [SloganWriter] Generating slogan for: {message}");
+ this._session ??= await this._agent.GetNewSessionAsync(cancellationToken);
+
+ AgentResponse result = await this._agent.RunAsync(message, this._session, cancellationToken: cancellationToken);
+ SloganResult slogan = JsonSerializer.Deserialize(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result.");
+ Console.WriteLine($" [SloganWriter] Generated: \"{slogan.Slogan}\"");
+ return slogan;
+ }
+
+ ///
+ /// Handles feedback from the feedback executor to refine the slogan.
+ ///
+ public async ValueTask HandleFeedbackAsync(FeedbackResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine($" [SloganWriter] Refining slogan based on feedback (rating was {message.Rating})...");
+
+ string feedbackMessage = $"""
+ Here is the feedback on your previous slogan:
+ Comments: {message.Comments}
+ Rating: {message.Rating}
+ Suggested Actions: {message.Actions}
+
+ Please use this feedback to improve your slogan.
+ """;
+
+ AgentResponse result = await this._agent.RunAsync(feedbackMessage, this._session, cancellationToken: cancellationToken);
+ SloganResult slogan = JsonSerializer.Deserialize(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result.");
+ Console.WriteLine($" [SloganWriter] Refined: \"{slogan.Slogan}\"");
+ return slogan;
+ }
+}