Adding azure functions support

This commit is contained in:
Shyju Krishnankutty
2026-02-12 14:49:48 -08:00
Unverified
parent e8d0bd9051
commit 310d1b8e10
41 changed files with 2626 additions and 26 deletions
@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>SequentialWorkflowFunctionApp</AssemblyName>
<RootNamespace>SequentialWorkflowFunctionApp</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace SequentialWorkflowFunctionApp;
public class Function
{
private readonly ILogger<Function> _logger;
public Function(ILogger<Function> logger)
{
_logger = logger;
}
[Function("Function")]
public IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req)
{
_logger.LogInformation("C# HTTP trigger function processed a request.");
return new OkObjectResult("Welcome to Azure Functions!");
}
}
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows;
namespace SequentialWorkflow;
/// <summary>
/// Parses an Order ID from a string input and returns an Order object populated.
/// </summary>
internal sealed class OrderLookup() : Executor<string, Order>("OrderLookup")
{
public override async ValueTask<Order> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
// Populate Order information from OrderId.
return new Order(message, 100.0m);
}
}
/// <summary>
/// Enriches an Order object with additional information.
/// </summary>
internal sealed class OrderEnrich() : Executor<Order, Order>("EnrichOrder")
{
public override async ValueTask<Order> HandleAsync(Order message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
if (message.Customer is null)
{
// populate customer information for the order from database.
message.Customer = new Customer(1, "Jerry");
}
return message;
}
}
internal sealed class PaymentProcessor() : Executor<Order, Order>("ProcessPayment")
{
public override async ValueTask<Order> HandleAsync(Order message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
message.PaymentReferenceNumber = Guid.NewGuid().ToString()[^4..];
return message;
}
}
internal sealed class Order
{
public Order(string id, decimal amount)
{
this.Id = id;
this.Amount = amount;
}
public string Id { get; }
public decimal Amount { get; }
public Customer? Customer { get; set; }
public string? PaymentReferenceNumber { get; set; }
}
public sealed record Customer(int Id, string Name);
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Extensions.Hosting;
using SequentialWorkflow;
OrderLookup orderLookupExecutor = new();
OrderEnrich orderEnricherExeecutor = new();
PaymentProcessor paymentProcessorExecutor = new();
Workflow fulfillOrder = new WorkflowBuilder(orderLookupExecutor)
.WithName("FulfillOrder")
.WithDescription("Looks up an order by ID and run payment processing")
.AddEdge(orderLookupExecutor, orderEnricherExeecutor)
.AddEdge(orderEnricherExeecutor, paymentProcessorExecutor)
.Build();
// Configure the function app to host the AI agent.
// This will automatically generate HTTP API endpoints for the agent.
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableOptions(durableOption =>
{
// Add a workflow.
durableOption.Workflows.AddWorkflow(fulfillOrder);
})
.Build();
app.Run();
@@ -0,0 +1,8 @@
# Default endpoint address for local testing
@authority=http://localhost:7071
### Prompt the agent
POST {{authority}}/api/workflows/FulfillOrder/run
Content-Type: text/plain
987
@@ -0,0 +1,20 @@
{
"version": "2.0",
"logging": {
"logLevel": {
"Microsoft.Agents.AI.DurableTask": "Information",
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
"DurableTask": "Information",
"Microsoft.DurableTask": "Information"
}
},
"extensions": {
"durableTask": {
"hubName": "default",
"storageProvider": {
"type": "AzureManaged",
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
}
}
}
}
@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>SequentialWorkflowFunctionApp</AssemblyName>
<RootNamespace>SequentialWorkflowFunctionApp</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows;
namespace WorkflowConcurrency;
/// <summary>
/// Parses and validates the incoming question before sending to AI agents.
/// </summary>
internal sealed class ParseQuestionExecutor() : Executor<string, string>("ParseQuestion")
{
public override ValueTask<string> HandleAsync(
string message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Magenta;
Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
Console.WriteLine("│ [ParseQuestion] Preparing question for AI agents...");
string formattedQuestion = message.Trim();
if (!formattedQuestion.EndsWith('?'))
{
formattedQuestion += "?";
}
Console.WriteLine($"│ [ParseQuestion] Question: \"{formattedQuestion}\"");
Console.WriteLine("│ [ParseQuestion] → Sending to Physicist and Chemist in PARALLEL...");
Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
Console.ResetColor();
return ValueTask.FromResult(formattedQuestion);
}
}
/// <summary>
/// Aggregates responses from all AI agents into a comprehensive answer.
/// This is the Fan-in point where parallel results are collected.
/// </summary>
internal sealed class AggregatorExecutor() : Executor<string[], string>("Aggregator")
{
public override ValueTask<string> HandleAsync(
string[] message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
Console.WriteLine($"│ [Aggregator] 📋 Received {message.Length} AI agent responses");
Console.WriteLine("│ [Aggregator] Combining into comprehensive answer...");
Console.WriteLine("│ [Aggregator] ✓ Aggregation complete!");
Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
Console.ResetColor();
string aggregatedResult = "═══════════════════════════════════════════════════════════════\n" +
" AI EXPERT PANEL RESPONSES\n" +
"═══════════════════════════════════════════════════════════════\n\n";
for (int i = 0; i < message.Length; i++)
{
string expertLabel = i == 0 ? "⚛️ PHYSICIST" : "🧪 CHEMIST";
aggregatedResult += $"{expertLabel}:\n{message[i]}\n\n";
}
aggregatedResult += "═══════════════════════════════════════════════════════════════\n" +
$"Summary: Received perspectives from {message.Length} AI experts.\n" +
"═══════════════════════════════════════════════════════════════";
return ValueTask.FromResult(aggregatedResult);
}
}
@@ -0,0 +1,53 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using OpenAI.Chat;
using WorkflowConcurrency;
// Configuration
string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
string? azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
// Create Azure OpenAI client
AzureOpenAIClient openAiClient = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
ChatClient chatClient = openAiClient.GetChatClient(deploymentName);
// Define the 4 executors for the workflow
ParseQuestionExecutor parseQuestion = new();
AIAgent physicist = chatClient.AsAIAgent("You are a physics expert. Be concise (2-3 sentences).", "Physicist");
AIAgent chemist = chatClient.AsAIAgent("You are a chemistry expert. Be concise (2-3 sentences).", "Chemist");
AggregatorExecutor aggregator = new();
// Build workflow: ParseQuestion -> [Physicist, Chemist] (parallel) -> Aggregator
Workflow workflow = new WorkflowBuilder(parseQuestion)
.WithName("ExpertReview")
.AddFanOutEdge(parseQuestion, [physicist, chemist])
.AddFanInEdge([physicist, chemist], aggregator)
.Build();
// Configure the function app to host the AI agent.
// This will automatically generate HTTP API endpoints for the agent.
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableOptions(durableOption =>
{
// Add a workflow.
durableOption.Workflows.AddWorkflow(workflow);
})
.Build();
app.Run();
@@ -0,0 +1,8 @@
# Default endpoint address for local testing
@authority=http://localhost:7071
### Prompt the agent
POST {{authority}}/api/workflows/ExpertReview/run
Content-Type: text/plain
What is saturation?
@@ -0,0 +1,20 @@
{
"version": "2.0",
"logging": {
"logLevel": {
"Microsoft.Agents.AI.DurableTask": "Information",
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
"DurableTask": "Information",
"Microsoft.DurableTask": "Information"
}
},
"extensions": {
"durableTask": {
"hubName": "default",
"storageProvider": {
"type": "AzureManaged",
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
}
}
}
}
@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>ConditionalEdgesFunctionApp</AssemblyName>
<RootNamespace>ConditionalEdgesFunctionApp</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,118 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows;
namespace ConditionalEdgesFunctionApp;
/// <summary>
/// Represents an order with customer and payment details.
/// </summary>
internal sealed class Order
{
public Order(string id, decimal amount)
{
this.Id = id;
this.Amount = amount;
}
public string Id { get; }
public decimal Amount { get; }
public Customer? Customer { get; set; }
public string? PaymentReferenceNumber { get; set; }
}
/// <summary>
/// Represents a customer associated with an order.
/// </summary>
public sealed record Customer(int Id, string Name, bool IsBlocked);
/// <summary>
/// Parses the order ID and retrieves order details.
/// </summary>
internal sealed class OrderIdParser() : Executor<string, Order>("OrderIdParser")
{
public override ValueTask<Order> HandleAsync(
string message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
Console.WriteLine($"[OrderIdParser] Parsing order ID: {message}");
Order order = new(message, 100.0m);
return ValueTask.FromResult(order);
}
}
/// <summary>
/// Enriches the order with customer information.
/// Orders with IDs containing 'B' are associated with blocked customers.
/// </summary>
internal sealed class OrderEnrich() : Executor<Order, Order>("EnrichOrder")
{
public override ValueTask<Order> HandleAsync(
Order message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
message.Customer = GetCustomerForOrder(message.Id);
Console.WriteLine($"[EnrichOrder] Customer: {message.Customer.Name}, IsBlocked: {message.Customer.IsBlocked}");
return ValueTask.FromResult(message);
}
private static Customer GetCustomerForOrder(string orderId)
{
if (orderId.Contains('B'))
{
return new Customer(101, "George", true);
}
return new Customer(201, "Jerry", false);
}
}
/// <summary>
/// Processes payment for valid (non-blocked) orders.
/// </summary>
internal sealed class PaymentProcessor() : Executor<Order, Order>("PaymentProcessor")
{
public override ValueTask<Order> HandleAsync(
Order message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
message.PaymentReferenceNumber = Guid.NewGuid().ToString()[..4];
Console.WriteLine($"[PaymentProcessor] Payment processed for order {message.Id}. Reference: {message.PaymentReferenceNumber}");
return ValueTask.FromResult(message);
}
}
/// <summary>
/// Notifies the fraud team when a blocked customer places an order.
/// </summary>
internal sealed class NotifyFraud() : Executor<Order, string>("NotifyFraud")
{
public override ValueTask<string> HandleAsync(
Order message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
string result = $"Order {message.Id} flagged as fraudulent for customer {message.Customer?.Name}.";
Console.WriteLine($"[NotifyFraud] {result}");
return ValueTask.FromResult(result);
}
}
/// <summary>
/// Defines condition functions for routing orders based on customer status.
/// </summary>
internal static class OrderRouteConditions
{
/// <summary>
/// Returns a condition that evaluates to true when the customer is blocked.
/// </summary>
internal static Func<Order?, bool> WhenBlocked() => order => order?.Customer?.IsBlocked == true;
/// <summary>
/// Returns a condition that evaluates to true when the customer is not blocked.
/// </summary>
internal static Func<Order?, bool> WhenNotBlocked() => order => order?.Customer?.IsBlocked == false;
}
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates conditional edges in a workflow hosted as an Azure Function.
// Orders are routed to different executors based on customer status:
// - Blocked customers → NotifyFraud
// - Valid customers → PaymentProcessor
using ConditionalEdgesFunctionApp;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
// Create executor instances
OrderIdParser orderParser = new();
OrderEnrich orderEnrich = new();
PaymentProcessor paymentProcessor = new();
NotifyFraud notifyFraud = new();
// Build workflow with conditional edges.
// The condition functions evaluate the Order output from OrderEnrich
// to determine whether to route to NotifyFraud or PaymentProcessor.
Workflow auditOrder = new WorkflowBuilder(orderParser)
.WithName("AuditOrder")
.WithDescription("Audits an order and routes based on customer status")
.AddEdge(orderParser, orderEnrich)
.AddEdge(orderEnrich, notifyFraud, condition: OrderRouteConditions.WhenBlocked())
.AddEdge(orderEnrich, paymentProcessor, condition: OrderRouteConditions.WhenNotBlocked())
.Build();
// Configure the function app to host the workflow.
// This will automatically generate HTTP API endpoints for the workflow.
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableOptions(durableOption =>
{
// Add the workflow.
durableOption.Workflows.AddWorkflow(auditOrder);
})
.Build();
app.Run();
@@ -0,0 +1,84 @@
# Conditional Edges Workflow - Azure Functions Sample
This sample demonstrates how to build a workflow with **conditional edges** hosted as an Azure Function. Orders are routed to different executors based on customer status.
## Key Concepts Demonstrated
- Building workflows with **conditional edges** using `AddEdge` with a `condition` parameter
- Hosting a conditional workflow as an Azure Function using `ConfigureDurableOptions`
- Defining reusable condition functions for routing logic
- Branching workflow execution based on data-driven decisions
## Overview
The workflow implements an order audit that routes orders differently based on whether the customer is blocked (flagged for fraud):
```
OrderIdParser --> OrderEnrich --[IsBlocked]--> NotifyFraud
|
+--[NotBlocked]--> PaymentProcessor
```
| Executor | Description |
|----------|-------------|
| OrderIdParser | Parses the order ID and retrieves order details |
| OrderEnrich | Enriches the order with customer information |
| PaymentProcessor | Processes payment for valid orders |
| NotifyFraud | Notifies the fraud team for blocked customers |
## How Conditional Edges Work
Conditional edges allow you to specify a condition function that determines whether the edge should be traversed:
```csharp
builder
.AddEdge(orderParser, orderEnrich)
.AddEdge(orderEnrich, notifyFraud, condition: OrderRouteConditions.WhenBlocked())
.AddEdge(orderEnrich, paymentProcessor, condition: OrderRouteConditions.WhenNotBlocked());
```
The condition functions receive the output of the source executor and return a boolean:
```csharp
internal static class OrderRouteConditions
{
internal static Func<Order?, bool> WhenBlocked() =>
order => order?.Customer?.IsBlocked == true;
internal static Func<Order?, bool> WhenNotBlocked() =>
order => order?.Customer?.IsBlocked == false;
}
```
### Routing Logic
- Order IDs containing the letter **'B'** are associated with blocked customers → routed to `NotifyFraud`
- All other order IDs are associated with valid customers → routed to `PaymentProcessor`
## Environment Setup
This sample requires:
- [.NET 10 SDK](https://dotnet.microsoft.com/download)
- [Azure Functions Core Tools v4](https://learn.microsoft.com/azure/azure-functions/functions-run-local)
- [Durable Task Scheduler](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-azure-managed-storage) running locally (default: `http://localhost:8080`)
- [Azurite](https://learn.microsoft.com/azure/storage/common/storage-use-azurite) for local Azure Storage emulation
## Running the Sample
```bash
cd dotnet/samples/Durable/Workflow/AzureFunctions/03_ConditionalEdges
func start
```
### Testing
**Valid order (routes to PaymentProcessor):**
```bash
curl -X POST http://localhost:7071/api/workflows/AuditOrder/run -H "Content-Type: text/plain" -d "12345"
```
**Blocked order (routes to NotifyFraud):**
```bash
curl -X POST http://localhost:7071/api/workflows/AuditOrder/run -H "Content-Type: text/plain" -d "12345B"
```
@@ -0,0 +1,14 @@
# Default endpoint address for local testing
@authority=http://localhost:7071
### Valid order (routes to PaymentProcessor)
POST {{authority}}/api/workflows/AuditOrder/run
Content-Type: text/plain
12345
### Blocked order (routes to NotifyFraud) - Order IDs containing 'B' are flagged
POST {{authority}}/api/workflows/AuditOrder/run
Content-Type: text/plain
12345B
@@ -0,0 +1,20 @@
{
"version": "2.0",
"logging": {
"logLevel": {
"Microsoft.Agents.AI.DurableTask": "Information",
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
"DurableTask": "Information",
"Microsoft.DurableTask": "Information"
}
},
"extensions": {
"durableTask": {
"hubName": "default",
"storageProvider": {
"type": "AzureManaged",
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
}
}
}
}
@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>NestedWorkflowsFunctionApp</AssemblyName>
<RootNamespace>NestedWorkflowsFunctionApp</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,220 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows;
namespace NestedWorkflowsFunctionApp;
// ============================================
// Order Processing Models
// ============================================
/// <summary>
/// Represents an order being processed through the workflow.
/// </summary>
internal sealed class OrderInfo
{
public required string OrderId { get; set; }
public decimal Amount { get; set; }
public string? PaymentTransactionId { get; set; }
public string? InventoryReservationId { get; set; }
public string? TrackingNumber { get; set; }
public string? Carrier { get; set; }
}
// ============================================
// Main Workflow Executors
// ============================================
/// <summary>
/// Entry point executor that receives the order ID and creates an OrderInfo object.
/// </summary>
internal sealed class OrderReceived() : Executor<string, OrderInfo>("OrderReceived")
{
public override ValueTask<OrderInfo> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
Console.WriteLine($"[OrderReceived] Processing order '{message}'");
OrderInfo order = new()
{
OrderId = message,
Amount = 99.99m
};
return ValueTask.FromResult(order);
}
}
/// <summary>
/// Final executor that outputs the completed order summary.
/// </summary>
internal sealed class OrderCompleted() : Executor<OrderInfo, string>("OrderCompleted")
{
public override ValueTask<string> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
Console.WriteLine($"[OrderCompleted] Order '{message.OrderId}' processed. Payment: {message.PaymentTransactionId}, Inventory: {message.InventoryReservationId}, Shipping: {message.Carrier} - {message.TrackingNumber}");
return ValueTask.FromResult($"Order {message.OrderId} completed. Tracking: {message.TrackingNumber}");
}
}
// ============================================
// Payment Sub-Workflow Executors
// ============================================
/// <summary>
/// Validates payment information for an order.
/// </summary>
internal sealed class ValidatePayment() : Executor<OrderInfo, OrderInfo>("ValidatePayment")
{
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
Console.WriteLine($"[Payment/ValidatePayment] Validating payment for order '{message.OrderId}'");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
Console.WriteLine($"[Payment/ValidatePayment] Payment validated for ${message.Amount}");
return message;
}
}
// ============================================
// Fraud Check Sub-Sub-Workflow Executors (Level 2 nesting)
// ============================================
/// <summary>
/// Analyzes transaction patterns for potential fraud.
/// </summary>
internal sealed class AnalyzePatterns() : Executor<OrderInfo, OrderInfo>("AnalyzePatterns")
{
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
Console.WriteLine($"[Payment/FraudCheck/AnalyzePatterns] Analyzing patterns for order '{message.OrderId}'");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
Console.WriteLine("[Payment/FraudCheck/AnalyzePatterns] Pattern analysis complete");
return message;
}
}
/// <summary>
/// Calculates a risk score for the transaction.
/// </summary>
internal sealed class CalculateRiskScore() : Executor<OrderInfo, OrderInfo>("CalculateRiskScore")
{
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
Console.WriteLine($"[Payment/FraudCheck/CalculateRiskScore] Calculating risk score for order '{message.OrderId}'");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
int riskScore = new Random().Next(1, 100);
Console.WriteLine($"[Payment/FraudCheck/CalculateRiskScore] Risk score: {riskScore}/100 (Low risk)");
return message;
}
}
/// <summary>
/// Charges the payment for an order.
/// </summary>
internal sealed class ChargePayment() : Executor<OrderInfo, OrderInfo>("ChargePayment")
{
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
Console.WriteLine($"[Payment/ChargePayment] Charging ${message.Amount} for order '{message.OrderId}'");
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
message.PaymentTransactionId = $"TXN-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
Console.WriteLine($"[Payment/ChargePayment] Payment processed: {message.PaymentTransactionId}");
return message;
}
}
// ============================================
// Inventory Sub-Workflow Executors
// ============================================
/// <summary>
/// Checks inventory availability for an order.
/// </summary>
internal sealed class CheckInventory() : Executor<OrderInfo, OrderInfo>("CheckInventory")
{
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
Console.WriteLine($"[Inventory/CheckInventory] Checking inventory for order '{message.OrderId}'");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
Console.WriteLine("[Inventory/CheckInventory] Items available in stock");
return message;
}
}
/// <summary>
/// Reserves inventory for an order.
/// </summary>
internal sealed class ReserveInventory() : Executor<OrderInfo, OrderInfo>("ReserveInventory")
{
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
Console.WriteLine($"[Inventory/ReserveInventory] Reserving items for order '{message.OrderId}'");
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
message.InventoryReservationId = $"RES-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
Console.WriteLine($"[Inventory/ReserveInventory] Reserved: {message.InventoryReservationId}");
return message;
}
}
// ============================================
// Shipping Sub-Workflow Executors
// ============================================
/// <summary>
/// Selects a shipping carrier for an order.
/// </summary>
internal sealed class SelectCarrier() : Executor<OrderInfo, OrderInfo>("SelectCarrier")
{
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
Console.WriteLine($"[Shipping/SelectCarrier] Selecting carrier for order '{message.OrderId}'");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
message.Carrier = message.Amount > 50 ? "Express" : "Standard";
Console.WriteLine($"[Shipping/SelectCarrier] Selected carrier: {message.Carrier}");
return message;
}
}
/// <summary>
/// Creates shipment and generates tracking number.
/// </summary>
internal sealed class CreateShipment() : Executor<OrderInfo, OrderInfo>("CreateShipment")
{
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
Console.WriteLine($"[Shipping/CreateShipment] Creating shipment for order '{message.OrderId}'");
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
message.TrackingNumber = $"TRACK-{Guid.NewGuid().ToString("N")[..10].ToUpperInvariant()}";
Console.WriteLine($"[Shipping/CreateShipment] Shipment created: {message.TrackingNumber}");
return message;
}
}
@@ -0,0 +1,122 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates nested workflows (sub-workflows) hosted as an Azure Function.
// One workflow can be used as an executor within another workflow, enabling modular,
// reusable workflow components with independent checkpointing and replay.
//
// Workflow structure:
//
// ┌─────────────────────────────────────────────────────────────────────────┐
// │ OrderProcessing (Main Workflow) │
// │ │
// │ OrderReceived │
// │ │ │
// │ ▼ │
// │ ┌─────────────────────────────────────────────────────────┐ │
// │ │ Payment (Sub-Workflow) │ │
// │ │ │ │
// │ │ ValidatePayment │ │
// │ │ │ │ │
// │ │ ▼ │ │
// │ │ ┌─────────────────────────────────────────┐ │ │
// │ │ │ FraudCheck (Sub-Sub-Workflow) │ │ │
// │ │ │ │ │ │
// │ │ │ AnalyzePatterns ──► CalculateRiskScore │ │ │
// │ │ └─────────────────────────────────────────┘ │ │
// │ │ │ │ │
// │ │ ▼ │ │
// │ │ ChargePayment │ │
// │ └─────────────────────────────────────────────────────────┘ │
// │ │ │
// │ ▼ │
// │ ┌─────────────────────────────────────────────────────────┐ │
// │ │ Inventory (Sub-Workflow) │ │
// │ │ │ │
// │ │ CheckInventory ──► ReserveInventory │ │
// │ └─────────────────────────────────────────────────────────┘ │
// │ │ │
// │ ▼ │
// │ ┌─────────────────────────────────────────────────────────┐ │
// │ │ Shipping (Sub-Workflow) │ │
// │ │ │ │
// │ │ SelectCarrier ──► CreateShipment │ │
// │ └─────────────────────────────────────────────────────────┘ │
// │ │ │
// │ ▼ │
// │ OrderCompleted │
// └─────────────────────────────────────────────────────────────────────────┘
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using NestedWorkflowsFunctionApp;
// Create executor instances for the Fraud Check sub-sub-workflow (Level 2 nesting)
AnalyzePatterns analyzePatterns = new();
CalculateRiskScore calculateRiskScore = new();
Workflow fraudCheckWorkflow = new WorkflowBuilder(analyzePatterns)
.WithName("SubFraudCheck")
.WithDescription("Analyzes transaction patterns and calculates risk score")
.AddEdge(analyzePatterns, calculateRiskScore)
.Build();
// Create executor instances for the Payment Processing sub-workflow
ValidatePayment validatePayment = new();
ExecutorBinding fraudCheckExecutor = fraudCheckWorkflow.BindAsExecutor("FraudCheck");
ChargePayment chargePayment = new();
Workflow paymentWorkflow = new WorkflowBuilder(validatePayment)
.WithName("SubPaymentProcessing")
.WithDescription("Validates and processes payment for an order")
.AddEdge(validatePayment, fraudCheckExecutor)
.AddEdge(fraudCheckExecutor, chargePayment)
.Build();
// Create executor instances for the Inventory Management sub-workflow
CheckInventory checkInventory = new();
ReserveInventory reserveInventory = new();
Workflow inventoryWorkflow = new WorkflowBuilder(checkInventory)
.WithName("SubInventoryManagement")
.WithDescription("Checks availability and reserves inventory")
.AddEdge(checkInventory, reserveInventory)
.Build();
// Create executor instances for the Shipping Arrangement sub-workflow
SelectCarrier selectCarrier = new();
CreateShipment createShipment = new();
Workflow shippingWorkflow = new WorkflowBuilder(selectCarrier)
.WithName("SubShippingArrangement")
.WithDescription("Selects carrier and creates shipment")
.AddEdge(selectCarrier, createShipment)
.Build();
// Build the main Order Processing workflow using sub-workflows as executors
ExecutorBinding paymentExecutor = paymentWorkflow.BindAsExecutor("Payment");
ExecutorBinding inventoryExecutor = inventoryWorkflow.BindAsExecutor("Inventory");
ExecutorBinding shippingExecutor = shippingWorkflow.BindAsExecutor("Shipping");
OrderReceived orderReceived = new();
OrderCompleted orderCompleted = new();
Workflow orderProcessingWorkflow = new WorkflowBuilder(orderReceived)
.WithName("OrderProcessing")
.WithDescription("Processes an order through payment, inventory, and shipping")
.AddEdge(orderReceived, paymentExecutor)
.AddEdge(paymentExecutor, inventoryExecutor)
.AddEdge(inventoryExecutor, shippingExecutor)
.AddEdge(shippingExecutor, orderCompleted)
.Build();
// Configure the function app to host the workflow.
// Sub-workflows are discovered and registered automatically.
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableOptions(durableOption =>
durableOption.Workflows.AddWorkflow(orderProcessingWorkflow))
.Build();
app.Run();
@@ -0,0 +1,112 @@
# Nested Workflows - Azure Functions Sample
This sample demonstrates how to build **nested workflows** (sub-workflows) hosted as an Azure Function. One workflow can be used as an executor within another workflow, enabling modular, reusable workflow components.
## Key Concepts Demonstrated
- Building workflows with **sub-workflow executors** using `BindAsExecutor`
- **Multi-level nesting** — a sub-workflow contains its own sub-workflow (FraudCheck inside Payment)
- Hosting nested workflows as an Azure Function using `ConfigureDurableOptions`
- Automatic discovery and registration of sub-workflows
## Overview
The workflow implements an order processing pipeline where each stage is a separate sub-workflow:
```
OrderReceived
┌─────────────────────────────────┐
│ Payment (Sub-Workflow) │
│ ValidatePayment │
│ ▼ │
│ ┌────────────────────────────┐ │
│ │ FraudCheck (Sub-Sub) │ │
│ │ AnalyzePatterns │ │
│ │ ▼ │ │
│ │ CalculateRiskScore │ │
│ └────────────────────────────┘ │
│ ▼ │
│ ChargePayment │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ Inventory (Sub-Workflow) │
│ CheckInventory ──► Reserve │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ Shipping (Sub-Workflow) │
│ SelectCarrier ──► CreateShip. │
└─────────────────────────────────┘
OrderCompleted
```
| Executor | Sub-Workflow | Description |
|----------|-------------|-------------|
| OrderReceived | Main | Receives order ID and creates OrderInfo |
| ValidatePayment | Payment | Validates payment information |
| AnalyzePatterns | FraudCheck (nested in Payment) | Analyzes transaction patterns |
| CalculateRiskScore | FraudCheck (nested in Payment) | Calculates fraud risk score |
| ChargePayment | Payment | Charges the payment |
| CheckInventory | Inventory | Checks item availability |
| ReserveInventory | Inventory | Reserves inventory items |
| SelectCarrier | Shipping | Selects shipping carrier |
| CreateShipment | Shipping | Creates shipment with tracking |
| OrderCompleted | Main | Outputs completed order summary |
## How Nested Workflows Work
Sub-workflows are created by binding a workflow as an executor:
```csharp
// Build a sub-workflow
Workflow paymentWorkflow = new WorkflowBuilder(validatePayment)
.WithName("SubPaymentProcessing")
.AddEdge(validatePayment, fraudCheckExecutor)
.AddEdge(fraudCheckExecutor, chargePayment)
.Build();
// Bind it as an executor in the parent workflow
ExecutorBinding paymentExecutor = paymentWorkflow.BindAsExecutor("Payment");
// Use it like any other executor
Workflow mainWorkflow = new WorkflowBuilder(orderReceived)
.AddEdge(orderReceived, paymentExecutor)
.Build();
```
Each sub-workflow runs as a separate orchestration instance, providing:
- **Modularity** — workflows can be composed from reusable sub-workflows
- **Independent checkpointing** — each sub-workflow has its own replay history
- **Hierarchical visualization** — view parent-child relationships in the DTS dashboard
- **Failure isolation** — sub-workflow failures don't corrupt parent state
## Environment Setup
This sample requires:
- [.NET 10 SDK](https://dotnet.microsoft.com/download)
- [Azure Functions Core Tools v4](https://learn.microsoft.com/azure/azure-functions/functions-run-local)
- [Durable Task Scheduler](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-azure-managed-storage) running locally (default: `http://localhost:8080`)
- [Azurite](https://learn.microsoft.com/azure/storage/common/storage-use-azurite) for local Azure Storage emulation
## Running the Sample
```bash
cd dotnet/samples/Durable/Workflow/AzureFunctions/04_NestedWorkflows
func start
```
### Testing
```bash
curl -X POST http://localhost:7071/api/workflows/OrderProcessing/run -H "Content-Type: text/plain" -d "ORD-2026-001"
```
Open the DTS dashboard at `http://localhost:8080` to see the parent-child orchestration hierarchy in the Timeline view.
@@ -0,0 +1,14 @@
# Default endpoint address for local testing
@authority=http://localhost:7071
### Process an order through nested workflows (Payment → Inventory → Shipping)
POST {{authority}}/api/workflows/OrderProcessing/run
Content-Type: text/plain
ORD-2026-001
### Process another order
POST {{authority}}/api/workflows/OrderProcessing/run
Content-Type: text/plain
ORD-2026-002
@@ -0,0 +1,20 @@
{
"version": "2.0",
"logging": {
"logLevel": {
"Microsoft.Agents.AI.DurableTask": "Information",
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
"DurableTask": "Information",
"Microsoft.DurableTask": "Information"
}
},
"extensions": {
"durableTask": {
"hubName": "default",
"storageProvider": {
"type": "AzureManaged",
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
}
}
}
}
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>NestedWorkflows</AssemblyName>
<RootNamespace>NestedWorkflows</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.DurableTask.Client.AzureManaged" />
<PackageReference Include="Microsoft.DurableTask.Worker.AzureManaged" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.DurableTask" />
<PackageReference Include="Microsoft.Agents.AI.Workflows" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,250 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows;
namespace NestedWorkflows;
// ============================================
// Order Processing Models
// ============================================
/// <summary>
/// Represents an order being processed through the workflow.
/// </summary>
internal sealed class OrderInfo
{
public required string OrderId { get; set; }
public decimal Amount { get; set; }
public string? PaymentTransactionId { get; set; }
public string? InventoryReservationId { get; set; }
public string? TrackingNumber { get; set; }
public string? Carrier { get; set; }
}
// ============================================
// Main Workflow Executors
// ============================================
/// <summary>
/// Entry point executor that receives the order ID and creates an OrderInfo object.
/// </summary>
internal sealed class OrderReceived() : Executor<string, OrderInfo>("OrderReceived")
{
public override ValueTask<OrderInfo> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"[OrderReceived] Processing order '{message}'");
Console.ResetColor();
OrderInfo order = new()
{
OrderId = message,
Amount = 99.99m
};
return ValueTask.FromResult(order);
}
}
/// <summary>
/// Final executor that outputs the completed order summary.
/// </summary>
internal sealed class OrderCompleted() : Executor<OrderInfo, string>("OrderCompleted")
{
public override ValueTask<string> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
Console.WriteLine($"│ [OrderCompleted] Order '{message.OrderId}' successfully processed!");
Console.WriteLine($"│ Payment: {message.PaymentTransactionId}");
Console.WriteLine($"│ Inventory: {message.InventoryReservationId}");
Console.WriteLine($"│ Shipping: {message.Carrier} - {message.TrackingNumber}");
Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
Console.ResetColor();
return ValueTask.FromResult($"Order {message.OrderId} completed. Tracking: {message.TrackingNumber}");
}
}
// ============================================
// Payment Sub-Workflow Executors
// ============================================
/// <summary>
/// Validates payment information for an order.
/// </summary>
internal sealed class ValidatePayment() : Executor<OrderInfo, OrderInfo>("ValidatePayment")
{
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($" [Payment/ValidatePayment] Validating payment for order '{message.OrderId}'...");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
Console.WriteLine($" [Payment/ValidatePayment] Payment validated for ${message.Amount}");
Console.ResetColor();
return message;
}
}
// ============================================
// Fraud Check Sub-Sub-Workflow Executors (Level 2 nesting)
// ============================================
/// <summary>
/// Analyzes transaction patterns for potential fraud.
/// </summary>
internal sealed class AnalyzePatterns() : Executor<OrderInfo, OrderInfo>("AnalyzePatterns")
{
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($" [Payment/FraudCheck/AnalyzePatterns] Analyzing patterns for order '{message.OrderId}'...");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
Console.WriteLine(" [Payment/FraudCheck/AnalyzePatterns] ✓ Pattern analysis complete");
Console.ResetColor();
return message;
}
}
/// <summary>
/// Calculates a risk score for the transaction.
/// </summary>
internal sealed class CalculateRiskScore() : Executor<OrderInfo, OrderInfo>("CalculateRiskScore")
{
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($" [Payment/FraudCheck/CalculateRiskScore] Calculating risk score for order '{message.OrderId}'...");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
int riskScore = new Random().Next(1, 100);
Console.WriteLine($" [Payment/FraudCheck/CalculateRiskScore] ✓ Risk score: {riskScore}/100 (Low risk)");
Console.ResetColor();
return message;
}
}
/// <summary>
/// Charges the payment for an order.
/// </summary>
internal sealed class ChargePayment() : Executor<OrderInfo, OrderInfo>("ChargePayment")
{
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($" [Payment/ChargePayment] Charging ${message.Amount} for order '{message.OrderId}'...");
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
message.PaymentTransactionId = $"TXN-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
Console.WriteLine($" [Payment/ChargePayment] ✓ Payment processed: {message.PaymentTransactionId}");
Console.ResetColor();
return message;
}
}
// ============================================
// Inventory Sub-Workflow Executors
// ============================================
/// <summary>
/// Checks inventory availability for an order.
/// </summary>
internal sealed class CheckInventory() : Executor<OrderInfo, OrderInfo>("CheckInventory")
{
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Magenta;
Console.WriteLine($" [Inventory/CheckInventory] Checking inventory for order '{message.OrderId}'...");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
Console.WriteLine(" [Inventory/CheckInventory] ✓ Items available in stock");
Console.ResetColor();
return message;
}
}
/// <summary>
/// Reserves inventory for an order.
/// </summary>
internal sealed class ReserveInventory() : Executor<OrderInfo, OrderInfo>("ReserveInventory")
{
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
Console.ForegroundColor = ConsoleColor.Magenta;
Console.WriteLine($" [Inventory/ReserveInventory] Reserving items for order '{message.OrderId}'...");
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
message.InventoryReservationId = $"RES-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
Console.WriteLine($" [Inventory/ReserveInventory] ✓ Reserved: {message.InventoryReservationId}");
Console.ResetColor();
return message;
}
}
// ============================================
// Shipping Sub-Workflow Executors
// ============================================
/// <summary>
/// Selects a shipping carrier for an order.
/// </summary>
internal sealed class SelectCarrier() : Executor<OrderInfo, OrderInfo>("SelectCarrier")
{
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Blue;
Console.WriteLine($" [Shipping/SelectCarrier] Selecting carrier for order '{message.OrderId}'...");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
message.Carrier = message.Amount > 50 ? "Express" : "Standard";
Console.WriteLine($" [Shipping/SelectCarrier] ✓ Selected carrier: {message.Carrier}");
Console.ResetColor();
return message;
}
}
/// <summary>
/// Creates shipment and generates tracking number.
/// </summary>
internal sealed class CreateShipment() : Executor<OrderInfo, OrderInfo>("CreateShipment")
{
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
Console.ForegroundColor = ConsoleColor.Blue;
Console.WriteLine($" [Shipping/CreateShipment] Creating shipment for order '{message.OrderId}'...");
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
message.TrackingNumber = $"TRACK-{Guid.NewGuid().ToString("N")[..10].ToUpperInvariant()}";
Console.WriteLine($" [Shipping/CreateShipment] ✓ Shipment created: {message.TrackingNumber}");
Console.ResetColor();
return message;
}
}
@@ -0,0 +1,228 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates nested workflows (sub-workflows) where one workflow
// can be used as an executor within another workflow. This enables modular,
// reusable workflow components with independent checkpointing and replay.
//
// Workflow structure:
//
// ┌─────────────────────────────────────────────────────────────────────────┐
// │ OrderProcessing (Main Workflow) │
// │ │
// │ OrderReceived │
// │ │ │
// │ ▼ │
// │ ┌─────────────────────────────────────────────────────────┐ │
// │ │ Payment (Sub-Workflow) │ │
// │ │ │ │
// │ │ ValidatePayment │ │
// │ │ │ │ │
// │ │ ▼ │ │
// │ │ ┌─────────────────────────────────────────┐ │ │
// │ │ │ FraudCheck (Sub-Sub-Workflow) │ │ │
// │ │ │ │ │ │
// │ │ │ AnalyzePatterns ──► CalculateRiskScore │ │ │
// │ │ └─────────────────────────────────────────┘ │ │
// │ │ │ │ │
// │ │ ▼ │ │
// │ │ ChargePayment │ │
// │ └─────────────────────────────────────────────────────────┘ │
// │ │ │
// │ ▼ │
// │ ┌─────────────────────────────────────────────────────────┐ │
// │ │ Inventory (Sub-Workflow) │ │
// │ │ │ │
// │ │ CheckInventory ──► ReserveInventory │ │
// │ └─────────────────────────────────────────────────────────┘ │
// │ │ │
// │ ▼ │
// │ ┌─────────────────────────────────────────────────────────┐ │
// │ │ Shipping (Sub-Workflow) │ │
// │ │ │ │
// │ │ SelectCarrier ──► CreateShipment │ │
// │ └─────────────────────────────────────────────────────────┘ │
// │ │ │
// │ ▼ │
// │ OrderCompleted │
// └─────────────────────────────────────────────────────────────────────────┘
//
// Each sub-workflow runs as a separate orchestration instance, providing:
// - Modular, reusable workflow components
// - Independent checkpointing and replay
// - Hierarchical visualization in the DTS dashboard
// - Failure isolation between parent and child workflows
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Agents.AI.DurableTask.Workflows;
using Microsoft.Agents.AI.Workflows;
using Microsoft.DurableTask.Client.AzureManaged;
using Microsoft.DurableTask.Worker.AzureManaged;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NestedWorkflows;
string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
// ============================================
// Step 1: Build the Fraud Check sub-sub-workflow (Level 2 nesting)
// ============================================
AnalyzePatterns analyzePatterns = new();
CalculateRiskScore calculateRiskScore = new();
Workflow fraudCheckWorkflow = new WorkflowBuilder(analyzePatterns)
.WithName("SubFraudCheck")
.WithDescription("Analyzes transaction patterns and calculates risk score")
.AddEdge(analyzePatterns, calculateRiskScore)
.Build();
// ============================================
// Step 2: Build the Payment Processing sub-workflow (with nested FraudCheck)
// ============================================
ValidatePayment validatePayment = new();
ExecutorBinding fraudCheckExecutor = fraudCheckWorkflow.BindAsExecutor("FraudCheck");
ChargePayment chargePayment = new();
Workflow paymentWorkflow = new WorkflowBuilder(validatePayment)
.WithName("SubPaymentProcessing")
.WithDescription("Validates and processes payment for an order")
.AddEdge(validatePayment, fraudCheckExecutor)
.AddEdge(fraudCheckExecutor, chargePayment)
.Build();
// ============================================
// Step 3: Build the Inventory Management sub-workflow
// ============================================
CheckInventory checkInventory = new();
ReserveInventory reserveInventory = new();
Workflow inventoryWorkflow = new WorkflowBuilder(checkInventory)
.WithName("SubInventoryManagement")
.WithDescription("Checks availability and reserves inventory")
.AddEdge(checkInventory, reserveInventory)
.Build();
// ============================================
// Step 4: Build the Shipping Arrangement sub-workflow
// ============================================
SelectCarrier selectCarrier = new();
CreateShipment createShipment = new();
Workflow shippingWorkflow = new WorkflowBuilder(selectCarrier)
.WithName("SubShippingArrangement")
.WithDescription("Selects carrier and creates shipment")
.AddEdge(selectCarrier, createShipment)
.Build();
// ============================================
// Step 5: Build the Main Order Processing workflow using sub-workflows
// ============================================
ExecutorBinding paymentExecutor = paymentWorkflow.BindAsExecutor("Payment");
ExecutorBinding inventoryExecutor = inventoryWorkflow.BindAsExecutor("Inventory");
ExecutorBinding shippingExecutor = shippingWorkflow.BindAsExecutor("Shipping");
OrderReceived orderReceived = new();
OrderCompleted orderCompleted = new();
// Main workflow: OrderReceived -> Payment -> Inventory -> Shipping -> OrderCompleted
Workflow orderProcessingWorkflow = new WorkflowBuilder(orderReceived)
.WithName("OrderProcessing")
.WithDescription("Processes an order through payment, inventory, and shipping")
.AddEdge(orderReceived, paymentExecutor)
.AddEdge(paymentExecutor, inventoryExecutor)
.AddEdge(inventoryExecutor, shippingExecutor)
.AddEdge(shippingExecutor, orderCompleted)
.Build();
// ============================================
// Step 6: Configure and start the host
// ============================================
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
.ConfigureServices(services =>
{
// Register only the main workflow - sub-workflows are discovered automatically!
services.ConfigureDurableWorkflows(
workflowOptions => workflowOptions.AddWorkflow(orderProcessingWorkflow),
workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
})
.Build();
await host.StartAsync();
IWorkflowClient workflowClient = host.Services.GetRequiredService<IWorkflowClient>();
Console.WriteLine("╔══════════════════════════════════════════════════════════════════╗");
Console.WriteLine("║ Nested Workflows Sample ║");
Console.WriteLine("╠══════════════════════════════════════════════════════════════════╣");
Console.WriteLine("║ Main Workflow: OrderProcessing ║");
Console.WriteLine("║ ├── Payment (sub-workflow) ║");
Console.WriteLine("║ │ ├── ValidatePayment ║");
Console.WriteLine("║ │ ├── FraudCheck (sub-sub-workflow) ← Level 2 nesting! ║");
Console.WriteLine("║ │ │ ├── AnalyzePatterns ║");
Console.WriteLine("║ │ │ └── CalculateRiskScore ║");
Console.WriteLine("║ │ └── ChargePayment ║");
Console.WriteLine("║ ├── Inventory (sub-workflow) ║");
Console.WriteLine("║ │ ├── CheckInventory ║");
Console.WriteLine("║ │ └── ReserveInventory ║");
Console.WriteLine("║ └── Shipping (sub-workflow) ║");
Console.WriteLine("║ ├── SelectCarrier ║");
Console.WriteLine("║ └── CreateShipment ║");
Console.WriteLine("╚══════════════════════════════════════════════════════════════════╝");
Console.WriteLine();
Console.WriteLine("Open the DTS dashboard at http://localhost:8080 to see the");
Console.WriteLine("parent-child orchestration hierarchy in the Timeline view!");
Console.WriteLine();
Console.WriteLine("Enter an order ID (or 'exit'):");
while (true)
{
Console.Write("> ");
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
{
break;
}
try
{
await StartNewWorkflowAsync(input, orderProcessingWorkflow, workflowClient);
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
Console.WriteLine();
}
await host.StopAsync();
// Start a new workflow and wait for completion
static async Task StartNewWorkflowAsync(string orderId, Workflow workflow, IWorkflowClient client)
{
Console.WriteLine($"\nStarting order processing for '{orderId}'...");
IAwaitableWorkflowRun run = (IAwaitableWorkflowRun)await client.RunAsync(workflow, orderId);
Console.WriteLine($"Run ID: {run.RunId}");
Console.WriteLine("Check the DTS dashboard Timeline tab to see sub-orchestrations!");
Console.WriteLine();
try
{
Console.WriteLine("Waiting for workflow to complete...");
string? result = await run.WaitForCompletionAsync<string>();
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"✓ Order completed: {result}");
Console.ResetColor();
}
catch (InvalidOperationException ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"✗ Failed: {ex.Message}");
Console.ResetColor();
}
}
@@ -0,0 +1,95 @@
# Nested Workflows Sample
This sample demonstrates how to use **nested workflows** (sub-workflows) where one workflow can be used as an executor within another workflow. This enables modular, reusable workflow components with independent checkpointing and replay.
## Key Concepts Demonstrated
- Using `Workflow.BindAsExecutor()` to embed a workflow as an executor in another workflow
- Multi-level nesting (sub-workflow within a sub-workflow)
- Automatic discovery of sub-workflows when registering the main workflow
- Independent orchestration instances for each sub-workflow
## Overview
The sample implements an order processing workflow with three sub-workflows and one sub-sub-workflow:
```
OrderProcessing (Main Workflow)
├── OrderReceived
├── Payment (sub-workflow)
│ ├── ValidatePayment
│ ├── FraudCheck (sub-sub-workflow) ← Level 2 nesting!
│ │ ├── AnalyzePatterns
│ │ └── CalculateRiskScore
│ └── ChargePayment
├── Inventory (sub-workflow)
│ ├── CheckInventory
│ └── ReserveInventory
├── Shipping (sub-workflow)
│ ├── SelectCarrier
│ └── CreateShipment
└── OrderCompleted
```
## How Nested Workflows Work
Sub-workflows are created using `BindAsExecutor()`, which converts a `Workflow` into an `ExecutorBinding` that can be used in another workflow's graph:
```csharp
// Build a sub-workflow
Workflow paymentWorkflow = new WorkflowBuilder(validatePayment)
.WithName("SubPaymentProcessing")
.AddEdge(validatePayment, fraudCheckExecutor)
.AddEdge(fraudCheckExecutor, chargePayment)
.Build();
// Bind as executor for use in the parent workflow
ExecutorBinding paymentExecutor = paymentWorkflow.BindAsExecutor("Payment");
// Use in the main workflow
Workflow mainWorkflow = new WorkflowBuilder(orderReceived)
.AddEdge(orderReceived, paymentExecutor)
.Build();
```
Each sub-workflow runs as a separate orchestration instance in the Durable Task Scheduler, visible in the DTS dashboard Timeline view.
## Environment Setup
See the [README.md](../README.md) file in the parent directory for information on configuring the environment, including how to install and run the Durable Task Scheduler.
## Running the Sample
```bash
cd dotnet/samples/Durable/Workflow/ConsoleApps/05_NestedWorkflows
dotnet run --framework net10.0
```
### Sample Output
```text
╔══════════════════════════════════════════════════════════════════╗
║ Nested Workflows Sample ║
╠══════════════════════════════════════════════════════════════════╣
║ Main Workflow: OrderProcessing ║
║ ├── Payment (sub-workflow) ║
║ │ ├── ValidatePayment ║
║ │ ├── FraudCheck (sub-sub-workflow) ← Level 2 nesting! ║
║ │ │ ├── AnalyzePatterns ║
║ │ │ └── CalculateRiskScore ║
║ │ └── ChargePayment ║
║ ├── Inventory (sub-workflow) ║
║ │ ├── CheckInventory ║
║ │ └── ReserveInventory ║
║ └── Shipping (sub-workflow) ║
║ ├── SelectCarrier ║
║ └── CreateShipment ║
╚══════════════════════════════════════════════════════════════════╝
Enter an order ID (or 'exit'):
> ORD-001
Starting order processing for 'ORD-001'...
Run ID: abc123...
Waiting for workflow to complete...
✓ Order completed: Order ORD-001 completed. Tracking: TRACK-1A2B3C4D5E
```