Note, the complete source code for the .NET version of this demo application is now available at https://github.com/JesseLiberty/blogMigration—public
In the previous post we finished up creating our agents. You’ll remember that each of the agents declared nodes. We’re finally going to put them to use in a class BlogWorkflow. However, up to now we’ve not fully taken advantage of the Microsoft Agent Framework (MAF). Let’s fix that up first. To do so we’ll add a class BlogExecutors. We’ll create MAF workflow executors that will wrap existing “node” chains so that the business logic is reused unchanged.
Note: In this post we’re only going to move towards Microsoft Agent Framework from where things currently stand. To see this fully migrated to an application that truly utilizes MAF, navigate to here

Let’s start with the BloggerExecutor which will allow the blogger to plan the task and seed the sub-task:
using Microsoft.Agents.AI.Workflows;
namespace BlogMigration;
/// <summary>Entry executor: lets the blogger plan the task and seed the sub-task.</summary>
internal sealed partial class BloggerExecutor(IBloggerChain blogger) : Executor("Blogger")
{
[MessageHandler]
private async ValueTask<ResearchState> HandleAsync(ResearchState state, IWorkflowContext context)
=> await blogger.BloggerNodeAsync(state);
}
An executor is a node in Microsoft Agent Framework. Each executor can receive messages, process them and emit new messages. In this case, whenever the workflow engine routes a ResearchState message to the executor, this method is invoked. The method takes the current workflow state and the workflow context and returns the updated ResearchState.
The executor is just a thin wrapper around the Blogger agent. The handler calls the Blogger node and the node updates the state.
Let’s go ahead and create the other Executors
/// <summary>Gathers research findings.</summary>
internal sealed partial class ResearcherExecutor(IResearcherAgent researcher) : Executor("Researcher")
{
[MessageHandler]
private async ValueTask<ResearchState> HandleAsync(ResearchState state, IWorkflowContext context)
=> await researcher.ResearchNodeAsync(state);
}
/// <summary>Writes or revises the draft (increments the revision counter).</summary>
internal sealed partial class AuthorExecutor(IAuthorChain author) : Executor("Author")
{
[MessageHandler]
private async ValueTask<ResearchState> HandleAsync(ResearchState state, IWorkflowContext context)
=> await author.AuthorNodeAsync(state);
}
/// <summary>
/// Reviews the draft and records approval / revision notes. Acts as the terminal
/// output node: when no further revision is needed it yields the final state.
/// </summary>
internal sealed partial class ReviewerExecutor(IReviewerChain reviewer) : Executor("Reviewer")
{
[MessageHandler]
private async ValueTask<ResearchState> HandleAsync(ResearchState state, IWorkflowContext context)
{
state = await reviewer.ReviewerNodeAsync(state);
if (!state.NeedsRevision)
{
// Approved, or the revision cap was hit — emit the final result.
await context.YieldOutputAsync(state);
}
// Returned state is routed back to the author only when the loop edge
// condition (NeedsRevision) is satisfied; otherwise it goes nowhere.
return state;
}
With that in place we can build the workflow. We begin by creating the executors as wrappers around our agents,
/// <summary>
/// Blogger → Researcher → Author → Reviewer
/// Reviewer ⇄ Author (bounded revision loop)
/// Reviewer → Output (on approval or revision cap)
///
/// The revision loop is bounded by <see cref="ResearchState.MaxRevisions"/>: the
/// loop-back edge only fires while the draft is unapproved AND the revision count
/// is below the cap, so the workflow is guaranteed to terminate even if the
/// reviewer never returns "APPROVED".
/// </summary>
public class BlogWorkflow(
IBloggerChain blogger,
IResearcherAgent researcher,
IAuthorChain author,
IReviewerChain reviewer) : IBlogWorkflow
{
public async Task<ResearchState> RunAsync(ResearchState state)
{
var bloggerExecutor = new BloggerExecutor(blogger);
var researcherExecutor = new ResearcherExecutor(researcher);
var authorExecutor = new AuthorExecutor(author);
var reviewerExecutor = new ReviewerExecutor(reviewer);
Now we create a Workflow object and add edges between our nodes
Workflow workflow = new WorkflowBuilder(bloggerExecutor)
.AddEdge(bloggerExecutor, researcherExecutor)
.AddEdge(researcherExecutor, authorExecutor)
.AddEdge(authorExecutor, reviewerExecutor)
.AddEdge<ResearchState>(reviewerExecutor, authorExecutor, condition: s => s?.NeedsRevision == true)
.WithOutputFrom(reviewerExecutor)
.Build();
Notice the conditional edge. This final AddEdge says “if the condition is not null and the draft needs revision (remember, the Reviewer caps the number of revisions at 4) then follow this edge. If NeedsRevision equals false, this edge is ignored.
We’re ready to let ‘er rip
Run run = await InProcessExecution.RunAsync(workflow, state);
foreach (WorkflowEvent evt in run.NewEvents)
{
if (evt is WorkflowOutputEvent { Data: ResearchState result })
{
return result;
}
}
return state;
}
All that’s left is to create Program.cs. We’ll begin by initializing the environment we need
using System.ClientModel;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using BlogMigration;
using Microsoft.Extensions.AI;
using OpenAI;
const string fileName = "config.json";
using var stream = File.OpenRead(fileName);
using var document = JsonDocument.Parse(stream);
JsonElement config = document.RootElement;
string? GetValue(string key) =>
config.TryGetProperty(key, out JsonElement value) ? value.GetString() : null;
Environment.SetEnvironmentVariable("OPENAI_API_KEY", GetValue("API_KEY"));
Environment.SetEnvironmentVariable("OPENAI_BASE_URL", GetValue("OPENAI_API_BASE"));
Environment.SetEnvironmentVariable("TAVILY_API_KEY", GetValue("TAVILY_API_KEY"));
string modelName = "gpt-4o-mini";
var openAIClient = new OpenAIClient(
new ApiKeyCredential(Environment.GetEnvironmentVariable("OPENAI_API_KEY")!),
new OpenAIClientOptions
{
Endpoint = new Uri(Environment.GetEnvironmentVariable("OPENAI_BASE_URL")!)
});
Now let’s instantiate our IChatClient (the LLM) and set the options. We’ll set temperature to 0 to have minimum variability and we’ll set the maximum output tokens.
IChatClient llm = openAIClient.GetChatClient(modelName).AsIChatClient();
var chatOptions = new ChatOptions
{
Temperature = 0,
MaxOutputTokens = 4096
};
Tavily is the library we’ll use for searching the web. You can learn more about it at https://tavily.com.
var tavilyHttpClient = new HttpClient { BaseAddress = new Uri("https://api.tavily.com/") };
tavilyHttpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("TAVILY_API_KEY"));
AIFunction tavilyTool = AIFunctionFactory.Create(
async (string query) =>
{
var request = new
{
query,
max_results = 5,
topic = "general",
include_answer = false,
include_raw_content = false,
search_depth = "basic"
};
using HttpResponseMessage response = await tavilyHttpClient.PostAsJsonAsync("search", request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
},
name: "tavily_search",
description: "A search engine optimized for comprehensive, accurate, and trusted results.");
Let’s set up all the agents and the BlogWorkflow
var bloggerChain = new BloggerChain(llm, chatOptions);
var researcherAgent = new ResearcherAgent(llm, chatOptions, tavilyTool);
var authorChain = new AuthorChain(llm, chatOptions);
var reviewerChain = new ReviewerChain(llm, chatOptions);
var app = new BlogWorkflow(bloggerChain, researcherAgent, authorChain, reviewerChain);
We’ll run the workflow for a sample topic
var initialState = new ResearchState
{
MainTask = "use of multiagents in writing a C# application"
};
ResearchState result = await app.RunAsync(initialState);
Console.WriteLine("\n========== RESULTS ==========");
Console.WriteLine($"Task: {result.MainTask}");
Console.WriteLine($"\nResearch Findings ({result.ResearchFindings.Count}):");
foreach (string finding in result.ResearchFindings)
{
Console.WriteLine($"- {finding}");
}
Console.WriteLine($"\nDraft:\n{result.Draft}");
Console.WriteLine($"\nReview Notes: {result.ReviewNotes}");
Console.WriteLine($"Revision Number: {result.RevisionNumber}");
Console.WriteLine("=============================");
Before we run this, here’s a look at the csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.10.0" />
<PackageReference Include="Microsoft.Agents.AI.Workflows.Generators" Version="1.10.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.AI" Version="10.7.0" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.7.0" />
<PackageReference Include="OpenAI" Version="2.11.0" />
</ItemGroup>
</Project>
That’s it. Easy Peasy.
Here is the output of running the above…
>>>Blogger
Blogger: No research yet, directing to researcher
Decision: researcher
Task: Research the topic: use of multiagents in writing a C# application
>>>RESEARCHER
Researching: Research the topic: use of multiagents in writing a C# application
Found: The search results highlight various resources and insights on developing multi-agent applications i...
>>>Author
Draft created: 3140 characters
>>REVIEWER
Review: APPROVED - The draft effectively introduces the concept of multi-agent systems in C# and provides va...
✓ Draft APPROVED
========== RESULTS ==========
Task: use of multiagents in writing a C# application
Research Findings (1):
- The search results highlight various resources and insights on developing multi-agent applications in C#.
1. **Creating a Multi-Agent Application**: Jesse Liberty's blog post introduces the concept of a multi-agent application, showcasing a test run of an application designed to generate blog posts. The post promises a detailed breakdown of the application in subsequent entries, indicating a focus on practical implementation.
2. **Semantic Kernel Framework**: A YouTube video discusses building a multi-agent application using the Semantic Kernel framework in C#. This resource likely provides a hands-on approach to leveraging this framework for multi-agent systems, emphasizing its capabilities and features.
3. **Microsoft Agent Framework**: Another YouTube tutorial presents the Microsoft Agent Framework as a robust alternative for creating multi-agent architectures in .NET. This resource suggests that the framework offers significant tools and functionalities for developers looking to implement multi-agent systems.
Overall, these findings suggest a growing interest in multi-agent systems within the C# ecosystem, with various frameworks and tutorials available to assist developers in creating sophisticated applications.
Draft:
# Harnessing Multi-Agent Systems in C#: A Comprehensive Guide
In the evolving landscape of software development, multi-agent systems (MAS) have emerged as a powerful paradigm, particularly in the realm of C#. These systems consist of multiple interacting agents, each capable of autonomous decision-making, which can lead to more efficient and scalable applications. This post explores the key resources and frameworks available for developing multi-agent applications in C#, providing a roadmap for developers interested in this innovative approach.
## Understanding Multi-Agent Applications
Multi-agent applications are designed to solve complex problems by distributing tasks among various agents. Each agent operates independently, yet they can collaborate to achieve common goals. Jesse Liberty's blog post serves as an excellent introduction to this concept, showcasing a practical example of a multi-agent application that generates blog posts. Liberty promises a detailed breakdown of the application in future entries, making it a valuable resource for developers looking to implement similar functionalities.
## Leveraging the Semantic Kernel Framework
For those seeking a hands-on approach, the Semantic Kernel framework offers a robust environment for building multi-agent applications in C#. A recent YouTube video delves into the capabilities of this framework, highlighting its features that facilitate the development of intelligent agents. The Semantic Kernel is particularly beneficial for developers aiming to integrate natural language processing and machine learning into their applications, allowing agents to understand and respond to user inputs more effectively.
## Exploring the Microsoft Agent Framework
Another noteworthy resource is the Microsoft Agent Framework, which provides a comprehensive set of tools for creating multi-agent architectures within the .NET ecosystem. A tutorial available on YouTube outlines the framework's functionalities, demonstrating how it can be utilized to develop sophisticated multi-agent systems. This framework is particularly advantageous for developers who are already familiar with the Microsoft stack, as it seamlessly integrates with existing .NET applications.
## Conclusion
The interest in multi-agent systems within the C# ecosystem is on the rise, driven by the need for more intelligent and responsive applications. With resources like Jesse Liberty's blog, the Semantic Kernel framework, and the Microsoft Agent Framework, developers have access to a wealth of knowledge and tools to create innovative multi-agent applications. As you embark on your journey into multi-agent development, these resources will serve as invaluable guides, helping you harness the full potential of this exciting technology.
By embracing multi-agent systems, you can elevate your applications, making them more dynamic and capable of handling complex tasks with ease. Whether you're generating content, automating processes, or enhancing user interactions, the possibilities are endless. Start exploring today and unlock the future of intelligent software development in C#.
Review Notes: APPROVED
Revision Number: 1
Note, I did not tell it to use my blog or what to say about it.





































