BrandGhost
MAF Workflows in C#: Sequential, Parallel, and Content Pipelines

MAF Workflows in C#: Sequential, Parallel, and Content Pipelines

MAF Workflows in C#: Sequential, Parallel, and Content Pipelines

Building real applications with the microsoft agent framework workflows csharp means thinking in terms of coordinated agent execution -- not just single prompts. A single agent that writes a draft is useful. A pipeline where a writer agent produces a draft, a fact-checker and grammar agent review it in parallel, and an editor agent consolidates everything into polished output? That's a production-worthy MAF workflow in C#.

This article walks through exactly that. We'll build on a real content pipeline application that uses MAF (Microsoft.Agents.AI v1.0.0-rc1) to orchestrate four specialized agents in a multi-step workflow. You'll see sequential execution, parallel execution with Task.WhenAll, and how to compose them into a MAF workflow in C# that's easy to reason about and extend.

If you're new to building agents with IChatClient, the Semantic Kernel in C# Complete AI Orchestration Guide provides useful background on the abstraction landscape. This article assumes you're comfortable with IChatClient and are ready to build multi-step workflows.

What "Workflow" Means in MAF

MAF doesn't ship with a built-in graph executor, workflow DSL, or pipeline runner -- not in v1.0.0-rc1, anyway. That's intentional. MAF's scope is the agent execution loop: wrapping an IChatClient with a system prompt, handling the tool-calling loop, and returning structured responses.

MAF workflows in C# are the coordination code you write: the C# that decides which agent runs first, which agents can run in parallel, how results flow from one step to the next, and what happens when something goes wrong. You are the orchestrator.

This is different from how Semantic Kernel approaches multi-agent orchestration with AgentGroupChat and selection strategies. The MAF workflow in C# approach is more explicit -- and for many use cases, that's a feature, not a gap. You get full control over execution order, branching, and error boundaries.

The Content Pipeline App

The content pipeline app (ContentPipeline) models a real content production workflow:

  1. A WriterAgent generates a 500-word technical draft from a topic
  2. A FactCheckerAgent and GrammarAgent review the draft simultaneously
  3. An EditorAgent consolidates both sets of feedback into a final polished article

Four agents. Four distinct system prompts. One IChatClient shared across all of them. The entry point in Program.cs is straightforward:

IChatClient chatClient = new OpenAIClient(apiKey)
    .GetChatClient(modelId)
    .AsIChatClient();

var pipeline = new ContentPipeline(chatClient);
var result = await pipeline.RunAsync(topic);

Console.WriteLine($"Final content saved to: {result.OutputPath}");
Console.WriteLine($"Total duration: {result.Duration.TotalSeconds:F1} seconds");

The ContentPipeline class takes a single IChatClient and creates all four agent instances internally. This makes setup simple and keeps provider configuration centralized.

Creating Agents with AsAIAgent()

Each agent in the pipeline wraps an IChatClient using the AsAIAgent() extension method from Microsoft.Agents.AI. This is the core MAF pattern:

using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

public sealed class WriterAgent
{
    private readonly ChatClientAgent _agent;

    public WriterAgent(IChatClient chatClient)
    {
        _agent = chatClient.AsAIAgent(
            instructions: "You are an expert technical writer specializing in software development topics. " +
                         "Write clear, accurate, and engaging content that is well-structured with an introduction, " +
                         "main points, and conclusion. Use specific examples where relevant.");
    }

    public async Task<string> GenerateDraftAsync(string topic, CancellationToken cancellationToken = default)
    {
        var prompt = $"Write a comprehensive 500-word technical article about: {topic}";
        var response = await _agent.RunAsync(prompt);

        var firstMessage = response.Messages.FirstOrDefault();
        if (firstMessage?.Contents != null)
        {
            var textContent = firstMessage.Contents
                .OfType<TextContent>()
                .FirstOrDefault();
            return textContent?.Text ?? string.Empty;
        }

        return string.Empty;
    }
}

A few things worth noting here. The instructions parameter becomes the agent's persistent system prompt -- it shapes every single response this agent produces. The AsAIAgent() call returns a ChatClientAgent instance. When you call RunAsync(), it returns an AgentResponse. The actual text lives in response.Messages[0].Contents, typed as TextContent, which you extract with OfType<TextContent>().

The FactCheckerAgent and GrammarAgent follow the exact same pattern -- only the system prompt differs. That's the power of agent specialization. Same infrastructure, radically different behavior.

Sequential Workflow: Step by Step

The first and last steps of the pipeline run sequentially by design. Step 1 (the writer) must complete before any reviews can happen -- you can't review something that doesn't exist yet. Step 3 (the editor) must wait for both reviews -- it needs all feedback before it can finalize.

The ContentPipeline.RunAsync method in ContentPipeline.cs makes this explicit:

public async Task<PipelineResult> RunAsync(string topic, CancellationToken cancellationToken = default)
{
    // Step 1: Sequential -- writer must run first
    Console.WriteLine("[Step 1/4] WriterAgent generating draft...");
    var draft = await _writerAgent.GenerateDraftAsync(topic, cancellationToken);

    // Step 2: Parallel reviews (covered next section)
    var (factCheck, grammarCheck) = await RunParallelReviewsAsync(draft, cancellationToken);

    // Step 3: Sequential -- editor consolidates both review results
    Console.WriteLine("[Step 3/4] EditorAgent consolidating and finalizing...");
    var finalContent = await _editorAgent.FinalizeAsync(draft, factCheck, grammarCheck, cancellationToken);

    // Step 4: Sequential -- save output
    var outputPath = await SaveOutputAsync(topic, finalContent);

    return new PipelineResult { /* ... */ };
}

The await keyword at each step is intentional. Each awaited call represents a sequential dependency -- a point where the next step genuinely cannot proceed until the current one completes. Sequential execution is the right choice whenever data from step N is required to start step N+1.

Parallel Workflow: Task.WhenAll

Steps 2a (fact checking) and 2b (grammar review) have no dependency on each other. They both take the same input -- the draft -- and produce independent outputs. Running them sequentially would waste time. This is the perfect case for Task.WhenAll:

private async Task<(string factCheck, string grammarCheck)> RunParallelReviewsAsync(
    string draft,
    CancellationToken cancellationToken)
{
    var factTask = Task.Run(async () =>
    {
        Console.WriteLine("  [2a] FactCheckerAgent reviewing...");
        var result = await _factCheckerAgent.ReviewAsync(draft, cancellationToken);
        Console.WriteLine("  Fact check complete");
        return result;
    }, cancellationToken);

    var grammarTask = Task.Run(async () =>
    {
        Console.WriteLine("  [2b] GrammarAgent reviewing...");
        var result = await _grammarAgent.ReviewAsync(draft, cancellationToken);
        Console.WriteLine("  Grammar check complete");
        return result;
    }, cancellationToken);

    await Task.WhenAll(factTask, grammarTask);

    return (await factTask, await grammarTask);
}

Both agents are invoked via Task.Run so their RunAsync calls fire simultaneously. Task.WhenAll blocks until both complete. Then you await each task individually to extract the results -- this is safe at this point since both tasks are already completed.

The net effect: instead of waiting for fact check then grammar review in series, both LLM calls are in-flight at the same time. For LLM-heavy workflows where each agent call takes multiple seconds, parallel execution can cut total runtime roughly in half.

The Full Pipeline: Combining Sequential and Parallel

Putting it together, the full pipeline looks like this flow:

WriterAgent (sequential)
    ↓
FactCheckerAgent ──┬── (parallel)
GrammarAgent   ───┘
    ↓
EditorAgent (sequential -- needs both review results)
    ↓
SaveOutput (sequential)

The EditorAgent.FinalizeAsync receives all three inputs: the original draft plus both review results:

public async Task<string> FinalizeAsync(
    string draft,
    string factCheckFeedback,
    string grammarFeedback,
    CancellationToken cancellationToken = default)
{
    var prompt = $"""
        Original draft:
        {draft}

        Fact checker feedback:
        {factCheckFeedback}

        Grammar and style feedback:
        {grammarFeedback}

        Please produce a final polished version incorporating all feedback 
        while maintaining technical accuracy and readability.
        """;

    var response = await _agent.RunAsync(prompt);

    var firstMessage = response.Messages.FirstOrDefault();
    if (firstMessage?.Contents != null)
    {
        var textContent = firstMessage.Contents
            .OfType<TextContent>()
            .FirstOrDefault();
        return textContent?.Text ?? string.Empty;
    }

    return string.Empty;
}

Notice that the EditorAgent's system prompt defines its role at construction time, but the actual review data flows in via the prompt at execution time. The agent knows how to edit (from its instructions), and what to edit (from the prompt). This separation of concerns -- instructions for behavior, prompt for data -- is a clean and repeatable MAF pattern.

Error Handling and Partial Failures

Parallel execution introduces a nuance: what happens when one of your parallel tasks fails? With Task.WhenAll, if any task throws, await Task.WhenAll(...) will rethrow the first exception. The other task's result -- even if it succeeded -- is no longer easily accessible.

For a pipeline where you can tolerate partial results (perhaps running the editor with only one of the two reviews), consider handling failures explicitly:

private async Task<(string factCheck, string grammarCheck)> RunParallelReviewsAsync(
    string draft,
    CancellationToken cancellationToken)
{
    var factTask = _factCheckerAgent.ReviewAsync(draft, cancellationToken);
    var grammarTask = _grammarAgent.ReviewAsync(draft, cancellationToken);

    string factCheck;
    string grammarCheck;

    try
    {
        factCheck = await factTask;
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Fact check failed: {ex.Message}");
        factCheck = "Fact check unavailable due to error.";
    }

    try
    {
        grammarCheck = await grammarTask;
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Grammar check failed: {ex.Message}");
        grammarCheck = "Grammar check unavailable due to error.";
    }

    return (factCheck, grammarCheck);
}

Here, each task is awaited independently. A failure in one doesn't prevent the other result from being used. The EditorAgent still runs -- just with degraded input. Whether this is the right trade-off depends on your application's quality requirements.

Sequential vs Parallel: Choosing the Right Approach

A few guidelines that emerge from working with the content pipeline pattern:

Use sequential execution when:

  • Step N requires the output of step N-1 as input
  • The steps have logical dependencies (you can't review what hasn't been written)
  • Ordering matters for correctness or quality

Use parallel execution when:

  • Multiple agents take the same input and produce independent outputs
  • Steps have no dependency on each other
  • Latency is a concern and your LLM provider supports concurrent requests

Watch for hidden dependencies. It's tempting to parallelize everything, but some dependencies are subtle. If two agents are writing to shared state (a file, a database, a shared object), parallel execution creates race conditions. The content pipeline avoids this by making each agent purely functional -- take a string in, return a string out.

Looking at how Semantic Kernel agents are structured shows another perspective on agent isolation -- the same principle applies to MAF: design agents that own their inputs and outputs cleanly.

MAF Workflows in C#: The Keyword in Context

Throughout the content pipeline, every step is an explicit MAF workflow in C# decision: which agent runs, in what order, and how data flows between them. The WriterAgent runs first because nothing else can start. The FactCheckerAgent and GrammarAgent run in parallel because they're independent. The EditorAgent runs last because it needs both reviews.

MAF workflows in C# don't require a framework plugin or a special API. They're just well-organized async C# with agents as the building blocks.

It's worth being explicit about something: MAF v1.0.0-rc1 does not include a built-in workflow engine, pipeline runner, or graph executor. There's no Pipeline.AddStep() API, no declarative workflow definition, no retry policies baked into the orchestration layer.

That's actually fine for most use cases. The ContentPipeline class is ~100 lines of plain C#. It's readable, debuggable, and easy to change. You can add a new step by adding a new agent and a new await call. You can swap the order of steps by moving two lines. You don't need to learn a new DSL to understand what the pipeline does.

The Strategy Design Pattern in C# is a useful mental model here: think of each agent as a strategy, and the pipeline as the context that decides which strategies run and in what order.

Putting It Together

The maf workflows in c# pattern comes down to a simple principle: each agent does one thing well, and you write the C# that decides how those things combine. Sequential for dependencies. Parallel for independence. Try/catch for resilience. No framework magic required.

The content pipeline app demonstrates that you can build a production-quality, multi-step MAF workflow in C# with four agent classes and a single orchestration class -- all in clear, idiomatic C#. The agents themselves are thin wrappers around IChatClient with specialized instructions. The orchestrator is just a method that calls them in the right order.

MAF workflows in C# scale gracefully. Add a new review step? Add a new agent and a new await. Need to speed things up? Identify which steps are independent and run them in parallel with Task.WhenAll. The pattern stays the same; only the number of agents and steps changes.

If your next step is building workflows where agents don't just pass content forward but critique and revise each other's work, that's the multi-agent revision loop pattern -- and it opens up a different dimension of quality control in AI pipelines.

Frequently Asked Questions

What is a MAF workflow in C# and how does it differ from a single agent?

A MAF workflow in C# is coordinated execution across multiple ChatClientAgent instances where the output of one agent feeds into the next. A single agent responds to a prompt. A workflow chains multiple agents -- sequentially or in parallel -- so each step builds on the previous one. The content pipeline in this article is a clear example: writer → parallel reviews → editor.

Does the Microsoft Agent Framework in C# have a built-in pipeline or workflow DSL?

No. As of v1.0.0-rc1, MAF workflows in C# are hand-coded using standard async/await patterns. You write the C# that decides which agent runs when, using await for sequential steps and Task.WhenAll for parallel steps. This is intentional -- it keeps the orchestration code explicit, readable, and easy to debug.

When should I use Task.WhenAll in a MAF workflow in C#?

Use Task.WhenAll when multiple agents take the same input and produce independent outputs. In the content pipeline, the fact-checker and grammar agent both receive the draft and produce separate reviews. Neither depends on the other's result. Running them in parallel with Task.WhenAll cuts the review step's elapsed time roughly in half.

How do I handle partial failures in MAF workflows in C#?

Rather than using await Task.WhenAll(...) -- which rethrows the first failure -- await each task individually inside a try/catch block. This lets you capture each result (or substitute a fallback) independently, so a failure in one parallel branch doesn't cancel the work done by the others.

Can MAF workflows in C# share a single IChatClient across multiple agents?

Yes. The content pipeline app demonstrates this: a single IChatClient is passed to the ContentPipeline constructor, and all four agents (WriterAgent, FactCheckerAgent, GrammarAgent, EditorAgent) share it. Each agent wraps the shared client with its own system prompt via AsAIAgent(). The IChatClient itself is stateless, so sharing it is safe.

Getting Started with Microsoft Agent Framework in C#

Getting started with Microsoft Agent Framework in C# is fast. Install packages, configure OpenAI or Azure OpenAI, and build your first streaming agent.

AgentSession and Multi-Turn Conversations in Microsoft Agent Framework

Master AgentSession and multi-turn conversations in Microsoft Agent Framework. Session lifecycle, concurrent sessions, and a complete stateful Q&A loop in C#.

Microsoft Agent Framework in C#: Complete Developer Guide

Complete guide to Microsoft Agent Framework in C#. Core abstractions, architecture, tool registration, sessions, and where MAF fits in the .NET AI ecosystem.

An error has occurred. This application may no longer respond until reloaded. Reload