Multi-Agent Workflows with Microsoft Agent Framework 1.0 and .NET 9

Build production-ready multi-agent systems with Microsoft Agent Framework 1.0 and .NET 9. Learn tool calling, orchestration patterns, and enterprise security.

0

Microsoft Agent Framework 1.0 shipped in April 2026 as a production-ready SDK for building autonomous agent systems in .NET and Python. Unlike Semantic Kernel, which abstracts LLM providers, Agent Framework handles the orchestration layer: managing agent state, routing between agents, executing tools reliably, and providing structured tracing for debugging. If you’ve been stitching together agent logic with custom state machines and prompt engineering, this framework changes what’s possible.

This article walks through building a production-ready multi-agent system: a document intake agent, a compliance checker, and an approval workflow. We’ll cover tool calling patterns, memory persistence, security, and the specific gotchas you’ll hit in production.

What Agent Framework 1.0 Actually Solves

Let’s be clear about scope. Agent Framework does not replace your LLM provider. It doesn’t make agents sentient or remove the need for careful prompt engineering. What it does is codify orchestration patterns that used to require custom code.

Before Agent Framework, you’d write agent loops like this:

while (!task_complete) {
  response = call_llm(system_prompt, history, tools)
  if (response.type == "tool_call") {
    result = execute_tool(response.tool_name, response.args)
    history.append(result)
  } else if (response.type == "final_answer") {
    task_complete = true
  }
}

This works for simple cases. But handling edge cases (concurrent tool calls, memory limits, human approval gates, retry logic, checkpointing) gets messy fast. Agent Framework provides declarative configuration for these patterns, backed by a graph-based execution engine.

Core Concepts: Agents, Tools, and Workflows

Three building blocks matter here.

Agents: Autonomous units that reason about a task using an LLM, decide which tools to call, and maintain execution state. Think of them as specialized workers.

Tools: Callable functions that agents invoke. Tools are registered with JSON Schema definitions so the LLM understands their contract.

Workflows: Directed graphs that connect agents and define handoff logic. Workflows handle inter-agent communication and state transitions using a superstep-based execution model.

Here’s a minimal example. We’ll build a document intake workflow with two agents:

using Microsoft.Agent;
using Microsoft.Agent.Orchestration;

public class DocumentIntakeWorkflow
{
    private readonly WorkflowBuilder _workflowBuilder;

    public async Task<WorkflowResult> ProcessDocumentAsync(string documentContent)
    {
        var workflow = new WorkflowBuilder("DocumentWorkflow");

        // Create intake agent
        var intakeAgent = new Agent("IntakeAgent")
        {
            Model = "gpt-4-turbo",
            SystemPrompt = "Extract key information: sender, document type, priority.",
            Tools = new[] { "extract_metadata", "validate_format" }
        };

        // Create compliance agent
        var complianceAgent = new Agent("ComplianceAgent")
        {
            Model = "gpt-4-turbo",
            SystemPrompt = "Check documents against UAE regulations and flag risks.",
            Tools = new[] { "check_regulations", "flag_risk" }
        };

        // Build workflow graph
        workflow.AddExecutor(intakeAgent);
        workflow.AddExecutor(complianceAgent);
        workflow.AddEdge("IntakeAgent", "ComplianceAgent");
        workflow.SetEndExecutor("ComplianceAgent");

        var executor = workflow.Build();
        var result = await executor.ExecuteAsync(new { Document = documentContent });

        return result;
    }
}

This declares two agents and a handoff: IntakeAgent processes the document, then passes it to ComplianceAgent. The framework handles state threading, LLM calls, and error recovery. Execution follows a superstep model where all triggered executors run concurrently within a superstep, then the workflow waits for all to complete before advancing.

Tool Calling: The Right Way

Tools are where agents interact with your systems. Agent Framework requires tools to be registered with JSON Schema definitions so the LLM knows their contract.

public class DocumentTools
{
    private readonly IDocumentService _documentService;

    public DocumentTools(IDocumentService documentService)
    {
        _documentService = documentService;
    }

    [Tool(
        Name = "extract_metadata",
        Description = "Extract structured metadata from a document. Returns sender name, document type (invoice, contract, report), priority level (low, medium, high), and creation date.",
        InputSchema = typeof(ExtractMetadataInput)
    )]
    public async Task<ToolResult> ExtractMetadata(ExtractMetadataInput input)
    {
        try
        {
            var metadata = await _documentService.ExtractAsync(input.DocumentId);
            return ToolResult.Success(new
            {
                sender = metadata.Sender,
                documentType = metadata.Type,
                priority = metadata.Priority,
                createdDate = metadata.CreatedDate
            });
        }
        catch (Exception ex)
        {
            return ToolResult.Failure($"Extraction failed: {ex.Message}");
        }
    }

    [Tool(
        Name = "validate_format",
        Description = "Validate document format and completeness. Checks for required fields, file integrity, and compliance with schema. Returns validation status and any errors found.",
        InputSchema = typeof(ValidateFormatInput)
    )]
    public async Task<ToolResult> ValidateFormat(ValidateFormatInput input)
    {
        var isValid = await _documentService.ValidateAsync(input.DocumentId);
        return isValid
            ? ToolResult.Success(new { valid = true })
            : ToolResult.Failure("Document format invalid or incomplete");
    }
}

public class ExtractMetadataInput
{
    [JsonPropertyName("document_id")]
    public string DocumentId { get; set; }
}

public class ValidateFormatInput
{
    [JsonPropertyName("document_id")]
    public string DocumentId { get; set; }
}

Notice the tool descriptions. They’re specific, not vague. The LLM sees “Extract structured metadata from a document. Returns sender name, document type (invoice, contract, report), priority level (low, medium, high), and creation date.” It doesn’t see “Process data.” Tool names are kebab-case for LLM clarity. Input classes use JSON property names matching your API schema. Tool results are explicit success or failure, never exceptions that bubble up.

Memory Management and State Persistence

Agents maintain conversation history and execution context. By default, Agent Framework keeps this in memory, which doesn’t scale. For production, you need persistent storage.

Agent Framework provides an AIContextProvider mechanism for injecting contextual information into an agent’s workflow. Here’s how to implement persistent memory:

public class PersistentAgentMemory : IMemoryStore
{
    private readonly IRedisClient _redis;

    public PersistentAgentMemory(IRedisClient redis)
    {
        _redis = redis;
    }

    public async Task AppendMessageAsync(string agentId, string message)
    {
        var key = $"agent:{agentId}:messages";
        await _redis.ListPushAsync(key, message);
        await _redis.KeyExpireAsync(key, TimeSpan.FromDays(30));
    }

    public async Task<List<string>> GetConversationAsync(string agentId, int limit = 50)
    {
        var key = $"agent:{agentId}:messages";
        var items = await _redis.ListRangeAsync(key, 0, limit - 1);
        return items.Select(i => i.ToString()).ToList();
    }

    public async Task ClearAsync(string agentId)
    {
        var key = $"agent:{agentId}:messages";
        await _redis.KeyDeleteAsync(key);
    }
}

// Register with DI
services.AddSingleton<IMemoryStore>(sp => 
    new PersistentAgentMemory(sp.GetRequiredService<IRedisClient>())
);

Use Redis or Cosmos DB for the backing store. Memory grows with conversation length, so implement TTL policies (e.g., purge after 30 days). For long-running workflows, periodically compress old messages into a summary the agent can reference.

Human-in-the-Loop Approval Workflows

Not everything should be automated. High-risk decisions (large approvals, compliance flags) need human review. Agent Framework provides approval patterns through function approval content types.

public class ApprovalWorkflow
{
    private readonly WorkflowBuilder _workflowBuilder;
    private readonly IApprovalService _approvalService;

    public async Task<WorkflowResult> ExecuteWithApprovalAsync(Document document)
    {
        var workflow = new WorkflowBuilder("DocumentWithApproval");

        var intakeAgent = new Agent("IntakeAgent")
        {
            Model = "gpt-4-turbo",
            SystemPrompt = "Extract document metadata."
        };

        var complianceAgent = new Agent("ComplianceAgent")
        {
            Model = "gpt-4-turbo",
            SystemPrompt = "Evaluate compliance risks."
        };

        workflow.AddExecutor(intakeAgent);
        workflow.AddExecutor(complianceAgent);
        workflow.AddEdge("IntakeAgent", "ComplianceAgent");

        // Add approval gate for high-risk documents
        var approvalExecutor = new ApprovalExecutor("HighRiskApproval",
            condition: (state) => state.ComplianceScore < 60,
            approvers: new[] { "compliance-team@company.ae" }
        );

        workflow.AddExecutor(approvalExecutor);
        workflow.AddEdge("ComplianceAgent", "HighRiskApproval");
        workflow.AddEdge("HighRiskApproval", "ProcessDocument", when: "approved");
        workflow.AddEdge("HighRiskApproval", "Reject", when: "rejected");

        var executor = workflow.Build();
        var result = await executor.ExecuteAsync(new { Document = document });

        return result;
    }
}

// In your API controller
[HttpPost("workflow/approve/{workflowId}")]
public async Task<IActionResult> ApproveAsync(string workflowId, [FromBody] ApprovalRequest request)
{
    var decision = new ApprovalDecision
    {
        WorkflowId = workflowId,
        Approved = request.Approved,
        Comment = request.Comment,
        ApprovedBy = User.FindFirst(ClaimTypes.Email)?.Value
    };

    await _approvalService.RecordDecisionAsync(decision);
    return Ok();
}

The approval executor pauses execution, notifies approvers, and resumes when a decision arrives. This pattern is essential for regulatory compliance in UAE enterprises.

RAG Integration: Grounding Agents in Your Data

Agents hallucinate without grounding. Retrieval-Augmented Generation embeds relevant documents into the agent’s context. Agent Framework integrates with vector databases through the AIContextProvider pattern.

public class RAGEnrichedAgent
{
    private readonly IVectorStore _vectorStore;
    private readonly IEmbeddingService _embeddings;

    public async Task<Agent> CreateComplianceAgentAsync()
    {
        var agent = new Agent("ComplianceAgent")
        {
            Model = "gpt-4-turbo",
            SystemPrompt = "You are a compliance expert. Use the provided regulations to evaluate documents."
        };

        // Inject context provider for RAG
        agent.ContextProvider = async (state) =>
        {
            var query = state.CurrentTask;
            var relevantDocs = await _vectorStore.SearchAsync("uae-regulations", query, topK: 5);
            var context = string.Join("\n", relevantDocs.Select(d => d.Text));
            return new AIContext
            {
                SystemInstructions = $"Base your analysis on these regulations:\n{context}"
            };
        };

        return agent;
    }
}

// Seed your vector store
public class RegulatoryDataSeeder
{
    private readonly IVectorStore _vectorStore;
    private readonly IEmbeddingService _embeddings;

    public async Task SeedUAERegulationsAsync()
    {
        var regulations = new[]
        {
            "UAE Labor Law 2021: Mandatory employee benefits include health insurance, annual leave, and end-of-service gratuity.",
            "DED Data Protection Requirements: Personal data must be stored securely and processed only for stated purposes.",
            "DFSA AML Regulations: Transaction thresholds require reporting for amounts exceeding AED 100,000."
        };

        foreach (var regulation in regulations)
        {
            var embedding = await _embeddings.EmbedAsync(regulation);
            await _vectorStore.InsertAsync("uae-regulations", new
            {
                Id = Guid.NewGuid(),
                Text = regulation,
                Embedding = embedding,
                Timestamp = DateTime.UtcNow
            });
        }
    }
}

The agent retrieves relevant regulations before reasoning, dramatically reducing hallucinations. Update your vector store regularly as regulations change.

Production Security Patterns

Agents execute code on your behalf. Security is non-negotiable.

Tool Execution Sandboxing

Restrict what tools agents can call and with what parameters:

public class ToolExecutionPolicy : IToolExecutionPolicy
{
    public async Task<bool> CanExecuteAsync(string toolName, Dictionary<string, object> args)
    {
        // Whitelist allowed tools
        var allowedTools = new[] { "extract_metadata", "validate_format", "check_regulations" };
        if (!allowedTools.Contains(toolName))
            return false;

        // Validate argument values
        if (toolName == "extract_metadata" && args.ContainsKey("document_id"))
        {
            var docId = args["document_id"] as string;
            if (string.IsNullOrEmpty(docId) || docId.Length > 100)
                return false;
        }

        return true;
    }
}

// Register
services.AddSingleton<IToolExecutionPolicy, ToolExecutionPolicy>();

API Rate Limiting and Token Budgets

Prevent runaway agent loops:

public class AgentExecutionLimits
{
    public int MaxToolCalls { get; set; } = 20;
    public int MaxConversationTokens { get; set; } = 8000;
    public TimeSpan ExecutionTimeout { get; set; } = TimeSpan.FromMinutes(5);
}

public class LimitedAgentExecutor : IAgentExecutor
{
    private readonly Agent _agent;
    private readonly AgentExecutionLimits _limits;

    public async Task<AgentResult> ExecuteAsync(AgentInput input)
    {
        var cts = new CancellationTokenSource(_limits.ExecutionTimeout);
        var toolCallCount = 0;
        var tokenCount = 0;

        while (!cts.Token.IsCancellationRequested)
        {
            if (toolCallCount >= _limits.MaxToolCalls)
                throw new InvalidOperationException("Tool call limit exceeded");

            if (tokenCount >= _limits.MaxConversationTokens)
                throw new InvalidOperationException("Token budget exceeded");

            var response = await _agent.ProcessAsync(input, cts.Token);
            tokenCount += response.TokensUsed;

            if (response.IsToolCall)
                toolCallCount++;
            else
                return response;
        }

        throw new OperationCanceledException("Agent execution timeout");
    }
}

Audit Logging

Log every agent decision for compliance:

public class AuditedAgentExecutor : IAgentExecutor
{
    private readonly Agent _agent;
    private readonly IAuditLog _auditLog;

    public async Task<AgentResult> ExecuteAsync(AgentInput input)
    {
        var executionId = Guid.NewGuid();
        var startTime = DateTime.UtcNow;

        await _auditLog.LogAsync(new AuditEntry
        {
            ExecutionId = executionId,
            AgentName = _agent.Name,
            Input = JsonSerializer.Serialize(input),
            Timestamp = startTime,
            Action = "ExecutionStarted"
        });

        try
        {
            var result = await _agent.ProcessAsync(input);

            await _auditLog.LogAsync(new AuditEntry
            {
                ExecutionId = executionId,
                AgentName = _agent.Name,
                Output = JsonSerializer.Serialize(result),
                Timestamp = DateTime.UtcNow,
                Action = "ExecutionCompleted",
                Status = "Success"
            });

            return result;
        }
        catch (Exception ex)
        {
            await _auditLog.LogAsync(new AuditEntry
            {
                ExecutionId = executionId,
                AgentName = _agent.Name,
                Error = ex.Message,
                Timestamp = DateTime.UtcNow,
                Action = "ExecutionFailed",
                Status = "Error"
            });

            throw;
        }
    }
}

Debugging and Observability

Agent workflows are hard to debug. Agent Framework provides structured tracing through OpenTelemetry integration:

var workflow = new WorkflowBuilder("DocumentWorkflow");

// Enable detailed tracing
workflow.EnableTracing(new TracingConfig
{
    LogLLMCalls = true,
    LogToolExecutions = true,
    LogStateTransitions = true,
    ExportTo = "Application Insights"
});

var executor = workflow.Build();
var result = await executor.ExecuteAsync(input);

// Query trace
var trace = result.Trace;
Console.WriteLine($"Agent: {trace.AgentName}");
Console.WriteLine($"LLM Calls: {trace.LLMCalls.Count}");
Console.WriteLine($"Tool Calls: {trace.ToolCalls.Count}");
foreach (var call in trace.ToolCalls)
{
    Console.WriteLine($"  - {call.ToolName}: {call.Status} ({call.DurationMs}ms)");
}

Export traces to Application Insights or DataDog for production monitoring. Set up alerts for execution timeouts, tool failures, or high error rates.

Common Gotchas and How to Avoid Them

Gotcha 1: Tool Descriptions Matter

The LLM chooses tools based on their descriptions. Vague descriptions lead to wrong tool calls.

Bad: “Process data”

Good: “Extract structured metadata from an uploaded document, including sender name, document type (invoice, contract, report), and priority level (low, medium, high). Returns JSON object with all fields.”

Gotcha 2: Memory Leaks with Long Conversations

Long agent conversations accumulate tokens. Implement conversation summarization:

public async Task SummarizeConversationAsync(string agentId)
{
    var messages = await _memory.GetConversationAsync(agentId);
    if (messages.Count > 100)
    {
        var oldMessages = messages.Take(50).ToList();
        var summary = await _summarizationService.SummarizeAsync(oldMessages);
        
        await _memory.ClearAsync(agentId);
        await _memory.AppendMessageAsync(agentId, $"Previous conversation summary: {summary}");
        
        foreach (var msg in messages.Skip(50))
            await _memory.AppendMessageAsync(agentId, msg);
    }
}

Gotcha 3: Tool Failures Cascade

If a tool fails, the agent keeps retrying. Add explicit retry limits and fallback strategies:

public class ResilientToolExecutor
{
    private readonly IToolProvider _toolProvider;
    private const int MaxRetries = 3;

    public async Task<ToolResult> ExecuteWithRetryAsync(string toolName, object args)
    {
        int attempt = 0;
        while (attempt < MaxRetries)
        {
            try
            {
                return await _toolProvider.ExecuteAsync(toolName, args);
            }
            catch (ToolExecutionException ex) when (ex.IsTransient && attempt < MaxRetries - 1)
            {
                attempt++;
                await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
            }
        }

        return ToolResult.Failure($"Tool {toolName} failed after {MaxRetries} attempts");
    }
}

Gotcha 4: LLM Latency in Synchronous Workflows

Agent Framework calls are async. If you block on them in synchronous code, you’ll deadlock. Always await:

// Bad
var result = executor.ExecuteAsync(input).Result; // Deadlock risk

// Good
var result = await executor.ExecuteAsync(input);

Putting It Together: A Complete Example

Here’s a minimal but complete ASP.NET Core API that orchestrates a multi-agent workflow:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Agent;
using Microsoft.Agent.Orchestration;

[ApiController]
[Route("api/[controller]")]
public class DocumentWorkflowController : ControllerBase
{
    private readonly WorkflowBuilder _workflowBuilder;
    private readonly IDocumentService _documentService;

    public DocumentWorkflowController(
        WorkflowBuilder workflowBuilder,
        IDocumentService documentService)
    {
        _workflowBuilder = workflowBuilder;
        _documentService = documentService;
    }

    [HttpPost("process")]
    public async Task<IActionResult> ProcessDocumentAsync([FromBody] DocumentUploadRequest request)
    {
        var workflow = new WorkflowBuilder("DocumentWorkflow");
        workflow.EnableTracing(new TracingConfig { LogLLMCalls = true });

        var intakeAgent = new Agent("IntakeAgent")
        {
            Model = "gpt-4-turbo",
            SystemPrompt = "Extract document metadata: sender, type, priority, amount if financial.",
            Tools = new[] { "extract_metadata", "validate_format" }
        };

        var complianceAgent = new Agent("ComplianceAgent")
        {
            Model = "gpt-4-turbo",
            SystemPrompt = "Evaluate compliance risks based on UAE regulations. Flag if risk score below 70.",
            Tools = new[] { "check_regulations", "flag_risk" }
        };

        workflow.AddExecutor(intakeAgent);
        workflow.AddExecutor(complianceAgent);
        workflow.AddEdge("IntakeAgent", "ComplianceAgent");
        workflow.SetEndExecutor("ComplianceAgent");

        var executor = workflow.Build();
        var result = await executor.ExecuteAsync(new { DocumentContent = request.Content });

        return Ok(new
        {
            WorkflowId = result.ExecutionId,
            Status = result.Status,
            Metadata = result.Output,
            ComplianceScore = result.FinalState["compliance_score"]
        });
    }
}

// Startup
public void ConfigureServices(IServiceCollection services)
{
    services.AddAgentFramework()
        .AddWorkflowOrchestration()
        .AddMemoryStore<RedisMemoryStore>()
        .AddVectorStore<AzureAISearchVectorStore>()
        .AddAuditLog<CosmosAuditLog>();

    services.AddScoped<IDocumentService, DocumentService>();
    services.AddScoped<DocumentTools>();
    services.AddScoped<ComplianceTools>();
}

Conclusion

Microsoft Agent Framework 1.0 is a solid foundation for building autonomous agent systems in .NET. It abstracts away orchestration boilerplate and provides production-ready patterns for tool calling, memory management, and approval workflows.

That said, it’s not a silver bullet. You still need strong prompt engineering, careful tool design, and robust error handling. The framework handles the plumbing, but you own the business logic.

For UAE enterprises building AI-driven automation, this framework bridges the gap between experimental LLM applications and production systems. Start with a single-agent workflow, validate it in staging, then layer on multi-agent orchestration and human approvals as confidence grows.

The competitive advantage isn’t the technology anymore. It’s execution. Get the framework right, and focus your energy on domain expertise and user experience.

Frequently Asked Questions

Q: How does Agent Framework compare to LangGraph or AutoGen?

A: LangGraph is language-agnostic and focuses on graph-based workflows. AutoGen emphasizes multi-agent conversation patterns. Agent Framework is .NET-native and includes built-in orchestration, memory persistence, and approval nodes. For .NET teams, it’s simpler to integrate with ASP.NET Core and Azure services. For heterogeneous stacks, LangGraph or AutoGen might be better. See [microsoft.com](https://github.com/microsoft/agent-framework) for comparison details.

Q: Do I need to rewrite my Semantic Kernel code?

A: No. Agent Framework complements Semantic Kernel; they’re not competitors. You can use Semantic Kernel for LLM abstraction and Agent Framework for orchestration. In fact, Agent Framework often wraps Semantic Kernel under the hood for LLM calls.

Q: What happens if an agent gets stuck in a loop?

A: Set execution limits (max tool calls, token budget, timeout). The framework enforces these and throws an exception if exceeded. Always log and alert on these failures so you can debug the agent’s system prompt or tool definitions.

Q: Can I use Agent Framework with on-premises LLMs?

A: Yes. Agent Framework accepts any LLM provider via the provider interface. You can wire it to Ollama, vLLM, or a local GPT-compatible service. Latency will be higher, but the pattern is the same.

Q: How do I handle sensitive data in agent conversations?

A: Never log raw sensitive data. Implement a data sanitizer that masks PII before storing conversation history. Use encryption at rest for memory stores and TLS for inter-agent communication. Consider field-level encryption for sensitive columns in audit logs.

Leave a Reply

Your email address will not be published. Required fields are marked *