Expose Your Workflow via API

Expose Jordan's multi-step triage workflow through Azure Functions to receive external alerts.

Foundational modules skipped

You've skipped some modules in this path. For a better understanding of the concepts used here, we recommend following the sequence.

Go to first skipped module

Overview

Jordan Miller’s incident triage assistant now has all the core building blocks: a defined Persona, a reasoning Brain, external Tools, persistent Memory, and a structured Workflow. The final step is to take this compound system out of the terminal and make it reachable by other systems.

In this final module of the Advanced Orchestration path, we will host the entire workflow as a Service using Azure Functions and Durable Agents. This transforms Jordan’s local orchestration into a scalable API endpoint that can be invoked by monitoring systems, Slack bots, or automated incident triggers.

System Anatomy

We have reached the final pillar: Hosting. This is the bridge that connects your compound AI system to the rest of the enterprise.

🔄
Workflows

Graph execution.

⚙️
Executors

Processing units.

🔀
Edges

Message routing.

📊
Events

Observability.

💾
State

Resiliency.

Building
☁️
Hosting

Service boundary.

graph TD
    subgraph API [The Service Boundary]
        direction TB
        Client[External Client]
        Trigger[HTTP Trigger]
        Workflow["Workflow (IncidentTriage)"]
        
        Client --> Trigger
        Trigger --> Workflow
    end
    
    subgraph Core [Execution Engine]
        Agent[TriageAgent]
        Executors[Logic Executors]
        
        Workflow --> Agent
        Workflow --> Executors
    end
    
    style API fill:#fff,stroke:#6366f1,stroke-width:2px,stroke-dasharray: 5 5
    style Workflow fill:#eef2ff,stroke:#4f46e5,stroke-width:2px
    style Core fill:#f8fafc,stroke:#64748b,stroke-dasharray: 2 2

The “Workflow-as-a-Service” Architecture

Most enterprise assistants don’t live in a console window. They live behind HTTP APIs. By hosting Jordan’s Triage Workflow in Azure Functions, we create a clear service boundary.

  • Accessibility: Any internal system can now “ask” for triage via standard JSON/HTTP.
  • Scalability: Azure Functions handle the heavy lifting of scaling the agent host.
  • Persistence: Leveraging the Durable backend ensures that workflow progress and agent memory are automatically preserved.

Setup your environment

Hosting a workflow as a service requires the Azure Functions Core Tools and a Durable Task backend.

📋 Pre-flight Checklist

  • 🛠️ Functions Core Tools: Ensure func is available in your terminal.
  • 📦 Hosting Extension: We use Microsoft.Agents.AI.Hosting.AzureFunctions.
  • 💾 Storage Backend: A Durable Task Scheduler (DTS) must be running.
PrerequisiteInfrastructure: Durable Task Scheduler (DTS)
Essential setup for local and cloud orchestration

The Agent Framework uses the Durable Task Scheduler (DTS) to manage the state and coordination of your workflows.

Configuration: host.json

Add the following to your project’s host.json to enable the Durable Task extension:

{
  "extensions": {
    "durableTask": {
      "storageProvider": {
        "type": "azureManaged",
        "connectionStringName": "DurableTaskSchedulerConnectionString"
      }
    }
  }
}

1. Start the Emulator

DTS Emulator

docker run -p 8080:8080 -p 8082:8082 mcr.microsoft.com/durabletask/scheduler-emulator:latest

Port 8080: gRPC API
Port 8082: Dashboard

2. Connection String

Add this to your local.settings.json:

“DurableTaskSchedulerConnectionString”: “Endpoint=http://localhost:8080;Authentication=None

1. Provision Resource

Deploy a Durable Task Scheduler instance via the Azure Portal or Bicep templates.

2. Assign Permissions

Enable Managed Identity on your Function App and assign it the Durable Task Data Contributor role on the DTS resource.

3. Connection String

Set this in your App Configuration or local.settings.json:

“DurableTaskSchedulerConnectionString”: “Endpoint=https://your-scheduler.durabletask.io;Authentication=ManagedIdentity

🛠️ Tooling: Azure Functions Core Tools

To scaffold and run your workflow host, you need the Azure Functions Core Tools (v4.x). If you don’t have them yet, follow the official installation guide for your platform.

🌐 Choosing Your Hosting Model

The Agent Framework leverages the Durable Task engine, giving you two primary hosting paths depending on your infrastructure needs:

  • Azure Functions (Durable Functions): What we use here. It offers deep Azure integration, built-in HTTP management, and flexible storage options.
  • Standalone SDKs: Ideal for self-hosting on any platform (Kubernetes, IoT Edge, etc.) where you want to manage the host yourself.

Learn more about choosing an orchestration framework.

Build the Host

To host our workflow, we use the Microsoft Agent Framework Hosting extension. We are exposing the entire multi-step Workflow we built previously as a reachable API.

1 Initialize the Project

First, create a new Azure Functions project using the Azure Functions Core Tools. We will use the Isolated Worker Model, which is the modern standard for .NET functions.

func init IncidentTriageWorkflow.Host --worker-runtime dotnet-isolated --target-framework net10.0
cd IncidentTriageWorkflow.Host
dotnet add package Microsoft.Agents.AI.Hosting.AzureFunctions
dotnet add package Microsoft.Agents.AI.OpenAI
dotnet add package Microsoft.Agents.AI.Workflows
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore
dotnet add package OpenAI
dotnet restore
Package Anatomy
☁️Microsoft.Agents.AI.Hosting.AzureFunctions

The hosting bridge. It provides the ConfigureDurableOptions extension to register agents and workflows as HTTP services.

🔄Microsoft.Agents.AI.Workflows

The core orchestration engine. Required for building multi-step assembly lines like Jordan’s incident triage.

💾Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged

The worker extension that enables the Function app to talk to the Durable Task Scheduler backend.

dotnet add package Microsoft.Agents.AI.Hosting.AzureFunctions
dotnet add package Microsoft.Agents.AI.OpenAI
dotnet add package Microsoft.Agents.AI.Workflows
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore
dotnet add package Azure.AI.OpenAI
dotnet add package Azure.Identity
dotnet restore
Package Anatomy
☁️Microsoft.Agents.AI.Hosting.AzureFunctions

The hosting bridge. It provides the ConfigureDurableOptions extension to register agents and workflows as HTTP services.

🔄Microsoft.Agents.AI.Workflows

The core orchestration engine. Required for building multi-step assembly lines like Jordan’s incident triage.

💾Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged

The worker extension that enables the Function app to talk to the Durable Task Scheduler backend.

2 Implement the Hosted Workflow

Replace the contents of Program.cs with the following code.

using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Hosting;
using OpenAI;
using OpenAI.Chat;
using System.ClientModel;
using System.ComponentModel;
using ChatMessage = Microsoft.Extensions.AI.ChatMessage;

// 1. Setup Jordan's Brain
var endpoint = Environment.GetEnvironmentVariable("OPENAI_ENDPOINT") ?? "http://localhost:1234/v1";
var modelName = Environment.GetEnvironmentVariable("OPENAI_MODEL_NAME") ?? "google/gemma-4-e4b";
var chatClient = new OpenAIClient(new ApiKeyCredential("dummy"), new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
    .GetChatClient(modelName);


AIAgent triageAgent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
    Name = "TriageAgent",
    ChatOptions = new()
    {
        Instructions = """
            You are Jordan's on-call assistant. Analyze telemetry and classify incident priority.
            """,
        Tools = [AIFunctionFactory.Create(AssessIncidentPriority, nameof(AssessIncidentPriority))]
    }
});

// 2. Build the Assembly Line (Workflow)
var fetchTelemetry = new FetchTelemetryExecutor();
var triageAdapter = new TriageAdapterExecutor();
var triageSync = new TriageSyncExecutor();
var escalationEngine = new EscalationExecutor();
var triageAgentExecutor = triageAgent.BindAsExecutor(new AIAgentHostOptions { ForwardIncomingMessages = false });

WorkflowBuilder builder = new(fetchTelemetry);
builder.WithName("IncidentTriage");
builder.AddEdge(fetchTelemetry, triageAdapter);
builder.AddEdge(triageAdapter, triageAgentExecutor);
builder.AddEdge(triageAgentExecutor, triageSync);
builder.AddEdge(triageSync, escalationEngine);

var workflow = builder.Build();

// 3. Start the Functions Host
using IHost app = FunctionsApplication
    .CreateBuilder(args)
    .ConfigureFunctionsWebApplication()
    .ConfigureDurableOptions(options =>
    {
        options.Workflows.AddWorkflow(workflow, exposeStatusEndpoint: true);
    })
    .Build();

app.Run();

// --- Logic & Executors ---
[Description("Classifies incident priority from service telemetry.")]
static string AssessIncidentPriority(string serviceName, double errorRatePercent, double latencyMs) => "PRIORITY: P0";

internal sealed class FetchTelemetryExecutor() : Executor<string, string>("FetchTelemetry")
{
    public override ValueTask<string> HandleAsync(string service, IWorkflowContext context, CancellationToken ct = default) => ValueTask.FromResult("[TELEMETRY] Latency: 450ms");
}

[SendsMessage(typeof(ChatMessage))]
internal sealed class TriageAdapterExecutor() : Executor<string>("TriageAdapter")
{
    public override async ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken ct = default) => await context.SendMessageAsync(new ChatMessage(ChatRole.User, message), ct);
}

internal sealed class TriageSyncExecutor() : Executor<string, string>("TriageSync")
{
    public override ValueTask<string> HandleAsync(string report, IWorkflowContext context, CancellationToken ct = default) => ValueTask.FromResult(report.Trim());
}

internal sealed class EscalationExecutor() : Executor<string, string>("EscalationEngine")
{
    public override ValueTask<string> HandleAsync(string report, IWorkflowContext context, CancellationToken ct = default) => ValueTask.FromResult("ALARM: Paging SRE Team.");
}
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Hosting;
using Azure.AI.OpenAI;
using Azure.Identity;
using OpenAI.Chat;
using System.ComponentModel;
using ChatMessage = Microsoft.Extensions.AI.ChatMessage;

// 1. Setup Jordan's Brain
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!;
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME")!;
var chatClient = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
    .GetChatClient(deploymentName);

AIAgent triageAgent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
    Name = "TriageAgent",
    ChatOptions = new()
    {
        Instructions = """
            You are Jordan's on-call assistant. Analyze telemetry and classify incident priority.
            """,
        Tools = [AIFunctionFactory.Create(AssessIncidentPriority, nameof(AssessIncidentPriority))]
    }
});

// 2. Build the Assembly Line (Workflow)
var fetchTelemetry = new FetchTelemetryExecutor();
var triageAdapter = new TriageAdapterExecutor();
var triageSync = new TriageSyncExecutor();
var escalationEngine = new EscalationExecutor();
var triageAgentExecutor = triageAgent.BindAsExecutor(new AIAgentHostOptions { ForwardIncomingMessages = false });

WorkflowBuilder builder = new(fetchTelemetry);
builder.WithName("IncidentTriage");
builder.AddEdge(fetchTelemetry, triageAdapter);
builder.AddEdge(triageAdapter, triageAgentExecutor);
builder.AddEdge(triageAgentExecutor, triageSync);
builder.AddEdge(triageSync, escalationEngine);

var workflow = builder.Build();

// 3. Start the Functions Host
using IHost app = FunctionsApplication
    .CreateBuilder(args)
    .ConfigureFunctionsWebApplication()
    .ConfigureDurableOptions(options =>
    {
        options.Workflows.AddWorkflow(workflow, exposeStatusEndpoint: true);
    })
    .Build();

app.Run();

// --- Logic & Executors ---
[Description("Classifies incident priority from service telemetry.")]
static string AssessIncidentPriority(string serviceName, double errorRatePercent, double latencyMs) => "PRIORITY: P0";

internal sealed class FetchTelemetryExecutor() : Executor<string, string>("FetchTelemetry")
{
    public override ValueTask<string> HandleAsync(string service, IWorkflowContext context, CancellationToken ct = default) => ValueTask.FromResult("[TELEMETRY] Latency: 450ms");
}

[SendsMessage(typeof(ChatMessage))]
internal sealed class TriageAdapterExecutor() : Executor<string>("TriageAdapter")
{
    public override async ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken ct = default) => await context.SendMessageAsync(new ChatMessage(ChatRole.User, message), ct);
}

internal sealed class TriageSyncExecutor() : Executor<string, string>("TriageSync")
{
    public override ValueTask<string> HandleAsync(string report, IWorkflowContext context, CancellationToken ct = default) => ValueTask.FromResult(report.Trim());
}

internal sealed class EscalationExecutor() : Executor<string, string>("EscalationEngine")
{
    public override ValueTask<string> HandleAsync(string report, IWorkflowContext context, CancellationToken ct = default) => ValueTask.FromResult("ALARM: Paging SRE Team.");
}

⚠️ Configuration Reminder

Ensure your local.settings.json contains the AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_DEPLOYMENT_NAME variables, as well as the DurableTaskSchedulerConnectionString required for the host to start.

🛡️ Hosted Workflow Turn Handling

In the in-process workflow from the previous module, TriageAdapter sends both a ChatMessage and a TurnToken. In this hosted Durable Functions workflow, send only the ChatMessage. The Durable host routes each emitted message across the workflow boundary, so sending a TurnToken here can schedule a second agent invocation with {“emitEvents”:true} as ordinary user input.

3 Start the Service

In your terminal, start the local Azure Functions runtime:

func start

Once the host is initialized, you will see a list of functions that have been automatically registered by the Agent Framework. These are grouped into Public API Endpoints and Internal Durable Machinery.

📡

External API Surface

These are the routes you will call from external systems.

🌐 Public Endpoints

POSThttp-IncidentTriage
/api/workflows/IncidentTriage/run
The entry point to trigger the triage workflow. Accepts incident details as a text payload.
GEThttp-IncidentTriage-status
/api/workflows/IncidentTriage/status/{runId}
Built-in endpoint to check the progress or final result of a specific workflow run.
⚙️

Internal Durable Machinery

These are the underlying triggers that manage the state and execution of the workflow graph. You generally don’t call these directly.

⚙️ Durable Triggers

dafx-IncidentTriage
Orchestration: Manages the sequence of steps and state of the triage pipeline.
dafx-[ExecutorName]
Activity: Individual task runners like FetchTelemetry, TriageSync, and EscalationEngine.
dafx-TriageAgent
Entity: The stateful brain of the assistant, maintaining Jordan’s persona and context.
graph LR
    Sys[External System] -->|POST run route| WorkflowHttp[http IncidentTriage]
    Sys -->|GET status route| StatusHttp[http IncidentTriage status]

    WorkflowHttp --> Orchestration[dafx IncidentTriage orchestration]
    StatusHttp --> Orchestration
    Orchestration --> Activities[Fetch Adapter Sync Escalation]
    Activities --> TriageEntity[dafx TriageAgent entity]
    TriageEntity --> Activities
    Activities --> Result[Triage result]

    style Sys fill:#f8fafc,stroke:#64748b,stroke-width:2px
    style WorkflowHttp fill:#fff7ed,stroke:#f97316,stroke-width:2px
    style StatusHttp fill:#fff7ed,stroke:#f97316,stroke-width:2px
    style Orchestration fill:#f9f9ff,stroke:#6366f1,stroke-width:2px
    style Activities fill:#f8fafc,stroke:#64748b,stroke-width:2px
    style TriageEntity fill:#eef2ff,stroke:#4f46e5,stroke-width:3px
    style Result fill:#ecfdf5,stroke:#22c55e,stroke-width:2px

The Service Architecture: A public HTTP surface backed by a Durable orchestration that coordinates multiple activities and a stateful Agent entity.

Try it: Invoke the Service

With the service running, you can now trigger the entire triage process from any HTTP client (like PowerShell, Postman, or a monitoring system).

The Agent Framework provides two primary ways to interact with your hosted workflows:

“Wait and See”

Force the connection to stay open until the workflow completes. This is the simplest way to test the full pipeline and see the final result immediately.

Invoke-RestMethod -Method Post `
  -Uri "http://localhost:7071/api/workflows/IncidentTriage/run" `
  -Headers @{ 
    "Content-Type" = "text/plain"; 
    "x-ms-wait-for-response" = "true" 
  } `
  -Body "checkout failures in production"

The Goal: Observe the terminal hang for a few seconds as the agent reasons, followed by the direct string response: ALARM: Paging SRE Team.

”Trigger and Poll”

Immediately receive a Run ID and check progress later. This is the recommended pattern for production systems and long-running workflows.

# 1. Trigger the workflow (returns a Run ID)
$run = Invoke-RestMethod -Method Post `
  -Uri "http://localhost:7071/api/workflows/IncidentTriage/run" `
  -Body "checkout failures in production"

$id = $run.runId

# 2. Poll the status endpoint
Invoke-RestMethod -Method Get `
  -Uri "http://localhost:7071/api/workflows/IncidentTriage/status/$id"

The Goal: See the status field move from Running to Completed. The JSON response provides the full audit trail of the orchestration.

Triage Different Payloads

Verify that the agent’s reasoning remains grounded by providing a “Healthy” scenario through the API.

Invoke-RestMethod -Method Post `
  -Uri "http://localhost:7071/api/workflows/IncidentTriage/run" `
  -Headers @{ "x-ms-wait-for-response" = "true" } `
  -Body "The payment service is 100% healthy"

The Goal: Even though you are invoking via HTTP, the underlying Triage Workflow still runs. The agent will analyze your text, see no issues, and return TICKET: Logged for triage.

Summary

Congratulations! You’ve completed the Advanced Orchestration path. Jordan Miller now has a fully autonomous, production-grade compound AI system that scales to meet the demands of any enterprise.

By mastering Executors, Workflows, and Hosting, you’ve moved beyond simple chat to building the next generation of AI-driven operational infrastructure.