Expose Your Workflow via API Done
Expose Jordan's multi-step triage workflow through Azure Functions to receive external alerts.
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.
Graph execution.
Processing units.
Message routing.
Observability.
Resiliency.
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 2The “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
funcis 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
docker run -p 8080:8080 -p 8082:8082 mcr.microsoft.com/durabletask/scheduler-emulator:latest
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.
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 restoreMicrosoft.Agents.AI.Hosting.AzureFunctionsThe hosting bridge. It provides the ConfigureDurableOptions extension to register agents and workflows as HTTP services.
Microsoft.Agents.AI.WorkflowsThe core orchestration engine. Required for building multi-step assembly lines like Jordan’s incident triage.
Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManagedThe 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 restoreMicrosoft.Agents.AI.Hosting.AzureFunctionsThe hosting bridge. It provides the ConfigureDurableOptions extension to register agents and workflows as HTTP services.
Microsoft.Agents.AI.WorkflowsThe core orchestration engine. Required for building multi-step assembly lines like Jordan’s incident triage.
Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManagedThe 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
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
FetchTelemetry, TriageSync, and EscalationEngine.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:2pxThe 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.