mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
sub workflow support
This commit is contained in:
@@ -51,6 +51,7 @@
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/06_LongRunningTools/06_LongRunningTools.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/07_ReliableStreaming/07_ReliableStreaming.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/08_SingleWorkflow/08_SingleWorkflow.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/09_SubWorkflows/09_SubWorkflows.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/09_Workflow_Concurrency/09_Workflow_Concurrency.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/10_Workflow_HITL/10_Workflow_HITL.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/11_WorkflowEvents/11_WorkflowEvents.csproj" />
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>SubWorkflows</AssemblyName>
|
||||
<RootNamespace>SubWorkflows</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.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,218 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace SubWorkflows;
|
||||
|
||||
// ============================================
|
||||
// Order Processing Models
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Represents an order being processed.
|
||||
/// </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 // Simulated order amount
|
||||
};
|
||||
|
||||
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}'...");
|
||||
Console.ResetColor();
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($" [Payment/ValidatePayment] Payment validated for ${message.Amount}");
|
||||
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}'...");
|
||||
Console.ResetColor();
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
|
||||
|
||||
message.PaymentTransactionId = $"TXN-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
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}'...");
|
||||
Console.ResetColor();
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
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}'...");
|
||||
Console.ResetColor();
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
|
||||
|
||||
message.InventoryReservationId = $"RES-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
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}'...");
|
||||
Console.ResetColor();
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
|
||||
message.Carrier = message.Amount > 50 ? "Express" : "Standard";
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Blue;
|
||||
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}'...");
|
||||
Console.ResetColor();
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
|
||||
|
||||
message.TrackingNumber = $"TRACK-{Guid.NewGuid().ToString("N")[..10].ToUpperInvariant()}";
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Blue;
|
||||
Console.WriteLine($" [Shipping/CreateShipment] ✓ Shipment created: {message.TrackingNumber}");
|
||||
Console.ResetColor();
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to use sub-workflows within a durable orchestration.
|
||||
// Sub-workflows allow you to compose complex workflows from simpler, reusable components.
|
||||
//
|
||||
// The sample implements an order processing workflow with three sub-workflows:
|
||||
// 1. PaymentProcessing - Validates and processes payment
|
||||
// 2. InventoryManagement - Checks and reserves inventory
|
||||
// 3. ShippingArrangement - Arranges shipping and generates tracking
|
||||
//
|
||||
// Each sub-workflow runs as a separate orchestration instance, visible in the DTS dashboard.
|
||||
// This provides:
|
||||
// - Modular, reusable workflow components
|
||||
// - Independent checkpointing and replay
|
||||
// - Hierarchical visualization in the dashboard
|
||||
// - Failure isolation between parent and child workflows
|
||||
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
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 SubWorkflows;
|
||||
|
||||
// Get DTS connection string from environment variable
|
||||
string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
|
||||
?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
|
||||
|
||||
// ============================================
|
||||
// Step 1: Build the Payment Processing sub-workflow
|
||||
// ============================================
|
||||
ValidatePayment validatePayment = new();
|
||||
ChargePayment chargePayment = new();
|
||||
|
||||
Workflow paymentWorkflow = new WorkflowBuilder(validatePayment)
|
||||
.WithName("SubPaymentProcessing")
|
||||
.WithDescription("Validates and processes payment for an order")
|
||||
.AddEdge(validatePayment, chargePayment)
|
||||
.Build();
|
||||
|
||||
// ============================================
|
||||
// Step 2: 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 3: 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 4: Build the Main Order Processing workflow using sub-workflows
|
||||
// ============================================
|
||||
// Bind sub-workflows as executors for use in the main workflow
|
||||
ExecutorBinding paymentExecutor = paymentWorkflow.BindAsExecutor("Payment");
|
||||
ExecutorBinding inventoryExecutor = inventoryWorkflow.BindAsExecutor("Inventory");
|
||||
ExecutorBinding shippingExecutor = shippingWorkflow.BindAsExecutor("Shipping");
|
||||
|
||||
// Create entry and exit executors for the main workflow
|
||||
OrderReceived orderReceived = new();
|
||||
OrderCompleted orderCompleted = new();
|
||||
|
||||
// Build the 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 5: 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(
|
||||
options => options.Workflows.AddWorkflow(orderProcessingWorkflow),
|
||||
workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
|
||||
clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
|
||||
})
|
||||
.Build();
|
||||
|
||||
await host.StartAsync();
|
||||
|
||||
// Get the IWorkflowClient from DI
|
||||
IWorkflowClient workflowClient = host.Services.GetRequiredService<IWorkflowClient>();
|
||||
|
||||
Console.WriteLine("╔══════════════════════════════════════════════════════════════════╗");
|
||||
Console.WriteLine("║ Durable Sub-Workflows Sample ║");
|
||||
Console.WriteLine("╠══════════════════════════════════════════════════════════════════╣");
|
||||
Console.WriteLine("║ Main Workflow: OrderProcessing ║");
|
||||
Console.WriteLine("║ ├── Payment (sub-workflow) ║");
|
||||
Console.WriteLine("║ │ ├── ValidatePayment (1s) ║");
|
||||
Console.WriteLine("║ │ └── ChargePayment (2s) ║");
|
||||
Console.WriteLine("║ ├── Inventory (sub-workflow) ║");
|
||||
Console.WriteLine("║ │ ├── CheckInventory (1s) ║");
|
||||
Console.WriteLine("║ │ └── ReserveInventory (2s) ║");
|
||||
Console.WriteLine("║ └── Shipping (sub-workflow) ║");
|
||||
Console.WriteLine("║ ├── SelectCarrier (1s) ║");
|
||||
Console.WriteLine("║ └── CreateShipment (2s) ║");
|
||||
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 using IWorkflowClient
|
||||
async Task StartNewWorkflowAsync(string orderId, Workflow workflow, IWorkflowClient client)
|
||||
{
|
||||
Console.WriteLine($"\nStarting order processing for '{orderId}'...");
|
||||
|
||||
await using DurableRun run = (DurableRun)await client.RunAsync(workflow, orderId);
|
||||
Console.WriteLine($"Instance ID: {run.InstanceId}");
|
||||
Console.WriteLine("Check the DTS dashboard Timeline tab to see sub-orchestrations!");
|
||||
Console.WriteLine();
|
||||
|
||||
try
|
||||
{
|
||||
string? result = await run.WaitForCompletionAsync();
|
||||
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,153 @@
|
||||
# Sub-Workflows Console Sample
|
||||
|
||||
This sample demonstrates how to compose workflows hierarchically by using sub-workflows within a durable orchestration. Sub-workflows are executed as separate orchestration instances, providing modularity, reusability, and excellent visibility in the Durable Task dashboard.
|
||||
|
||||
## Overview
|
||||
|
||||
The sample implements an order processing system with three sub-workflows:
|
||||
|
||||
```
|
||||
OrderProcessing (Main Workflow)
|
||||
??? OrderReceived
|
||||
??? Payment (Sub-Workflow)
|
||||
? ??? ValidatePayment (1s)
|
||||
? ??? ChargePayment (2s)
|
||||
??? Inventory (Sub-Workflow)
|
||||
? ??? CheckInventory (1s)
|
||||
? ??? ReserveInventory (2s)
|
||||
??? Shipping (Sub-Workflow)
|
||||
? ??? SelectCarrier (1s)
|
||||
? ??? CreateShipment (2s)
|
||||
??? OrderCompleted
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Sub-Workflow Benefits
|
||||
|
||||
1. **Modularity**: Each sub-workflow encapsulates related logic (payment, inventory, shipping)
|
||||
2. **Reusability**: Sub-workflows can be used in multiple parent workflows
|
||||
3. **Independent Execution**: Each sub-workflow runs as a separate orchestration instance
|
||||
4. **Dashboard Visibility**: Sub-workflows appear in the Timeline view with parent-child relationships
|
||||
5. **Failure Isolation**: A failure in a sub-workflow doesn't corrupt the parent's state
|
||||
|
||||
### How Sub-Workflows Work
|
||||
|
||||
```csharp
|
||||
// Step 1: Build a sub-workflow
|
||||
Workflow paymentWorkflow = new WorkflowBuilder(validatePayment)
|
||||
.WithName("PaymentProcessing")
|
||||
.AddEdge(validatePayment, chargePayment)
|
||||
.Build();
|
||||
|
||||
// Step 2: Bind it as an executor for use in a parent workflow
|
||||
ExecutorBinding paymentExecutor = paymentWorkflow.BindAsExecutor("Payment");
|
||||
|
||||
// Step 3: Use the sub-workflow executor in the main workflow
|
||||
Workflow mainWorkflow = new WorkflowBuilder(orderReceived)
|
||||
.AddEdge(orderReceived, paymentExecutor) // Sub-workflow as an edge target
|
||||
.AddEdge(paymentExecutor, inventoryExecutor)
|
||||
.Build();
|
||||
|
||||
// Step 4: Register only the main workflow - sub-workflows are discovered automatically!
|
||||
services.ConfigureDurableWorkflows(
|
||||
options => options.Workflows.AddWorkflow(mainWorkflow),
|
||||
...);
|
||||
```
|
||||
|
||||
### Dashboard Visualization
|
||||
|
||||
Open the DTS dashboard at `http://localhost:8080` after running a workflow:
|
||||
|
||||
1. Click on the main orchestration instance
|
||||
2. Switch to the **Timeline** tab
|
||||
3. You'll see a hierarchical view showing:
|
||||
- `OrderProcessing` (parent orchestration)
|
||||
- `PaymentProcessing` (sub-orchestration)
|
||||
- `InventoryManagement` (sub-orchestration)
|
||||
- `ShippingArrangement` (sub-orchestration)
|
||||
|
||||
Each sub-orchestration has its own instance ID and can be inspected independently.
|
||||
|
||||
## Environment Setup
|
||||
|
||||
See the [README.md](../README.md) file in the parent directory for information on:
|
||||
- Installing prerequisites (.NET 10+, Docker)
|
||||
- Starting the Durable Task Scheduler emulator
|
||||
- Configuring environment variables
|
||||
|
||||
## Running the Sample
|
||||
|
||||
```bash
|
||||
# Start the DTS emulator (if not already running)
|
||||
docker run -d --name dts-emulator -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest
|
||||
|
||||
# Run the sample
|
||||
cd dotnet/samples/DurableAgents/ConsoleApps/09_SubWorkflows
|
||||
dotnet run --framework net10.0
|
||||
```
|
||||
|
||||
### Sample Session
|
||||
|
||||
```text
|
||||
????????????????????????????????????????????????????????????????????
|
||||
? Durable Sub-Workflows Sample ?
|
||||
????????????????????????????????????????????????????????????????????
|
||||
? Main Workflow: OrderProcessing ?
|
||||
? ??? Payment (sub-workflow) ?
|
||||
? ? ??? ValidatePayment (1s) ?
|
||||
? ? ??? ChargePayment (2s) ?
|
||||
? ??? Inventory (sub-workflow) ?
|
||||
? ? ??? CheckInventory (1s) ?
|
||||
? ? ??? ReserveInventory (2s) ?
|
||||
? ??? Shipping (sub-workflow) ?
|
||||
? ??? SelectCarrier (1s) ?
|
||||
? ??? CreateShipment (2s) ?
|
||||
????????????????????????????????????????????????????????????????????
|
||||
|
||||
Open the DTS dashboard at http://localhost:8080 to see the
|
||||
parent-child orchestration hierarchy in the Timeline view!
|
||||
|
||||
Enter an order ID (or 'exit'):
|
||||
> ORD-12345
|
||||
|
||||
Starting order processing for 'ORD-12345'...
|
||||
Instance ID: abc123def456
|
||||
Check the DTS dashboard Timeline tab to see sub-orchestrations!
|
||||
|
||||
[OrderReceived] Processing order 'ORD-12345'
|
||||
[Payment/ValidatePayment] Validating payment for order 'ORD-12345'...
|
||||
[Payment/ValidatePayment] Payment validated for $99.99
|
||||
[Payment/ChargePayment] Charging $99.99 for order 'ORD-12345'...
|
||||
[Payment/ChargePayment] ? Payment processed: TXN-A1B2C3D4
|
||||
[Inventory/CheckInventory] Checking inventory for order 'ORD-12345'...
|
||||
[Inventory/CheckInventory] ? Items available in stock
|
||||
[Inventory/ReserveInventory] Reserving items for order 'ORD-12345'...
|
||||
[Inventory/ReserveInventory] ? Reserved: RES-E5F6G7H8
|
||||
[Shipping/SelectCarrier] Selecting carrier for order 'ORD-12345'...
|
||||
[Shipping/SelectCarrier] ? Selected carrier: Express
|
||||
[Shipping/CreateShipment] Creating shipment for order 'ORD-12345'...
|
||||
[Shipping/CreateShipment] ? Shipment created: TRACK-I9J0K1L2M3
|
||||
???????????????????????????????????????????????????????????????????
|
||||
? [OrderCompleted] Order 'ORD-12345' successfully processed!
|
||||
? Payment: TXN-A1B2C3D4
|
||||
? Inventory: RES-E5F6G7H8
|
||||
? Shipping: Express - TRACK-I9J0K1L2M3
|
||||
???????????????????????????????????????????????????????????????????
|
||||
? Order completed. Tracking: TRACK-I9J0K1L2M3
|
||||
```
|
||||
|
||||
## Comparison with In-Process Sub-Workflows
|
||||
|
||||
| Feature | In-Process | Durable |
|
||||
|---------|------------|---------|
|
||||
| Execution | Same process, synchronized supersteps | Separate orchestration instances |
|
||||
| Visibility | Single workflow view | Hierarchical dashboard view |
|
||||
| Checkpointing | Parent checkpoints include child state | Independent checkpoints per sub-workflow |
|
||||
| Failure Recovery | Parent must handle child failures | Automatic retry with state preservation |
|
||||
| Scalability | Single process | Can scale across workers |
|
||||
|
||||
## Related Samples
|
||||
|
||||
- [06_SubWorkflows (In-Process)](../../../GettingStarted/Workflows/_Foundational/06_SubWorkflows) - In-process sub-workflow execution
|
||||
- [08_SingleWorkflow](../08_SingleWorkflow) - Basic durable workflow without sub-workflows
|
||||
@@ -9,6 +9,8 @@ This directory contains samples for console app hosting of durable agents. These
|
||||
- **[05_AgentOrchestration_HITL](05_AgentOrchestration_HITL)**: A sample that demonstrates how to implement a human-in-the-loop workflow using durable orchestration, including interactive approval prompts.
|
||||
- **[06_LongRunningTools](06_LongRunningTools)**: A sample that demonstrates how agents can start and interact with durable orchestrations from tool calls to enable long-running tool scenarios.
|
||||
- **[07_ReliableStreaming](07_ReliableStreaming)**: A sample that demonstrates how to implement reliable streaming for durable agents using Redis Streams, enabling clients to disconnect and reconnect without losing messages.
|
||||
- **[08_SingleWorkflow](08_SingleWorkflow)**: A sample that demonstrates how to run a simple workflow as a durable orchestration, showcasing activity durability and automatic resume on restart.
|
||||
- **[09_SubWorkflows](09_SubWorkflows)**: A sample that demonstrates how to compose workflows hierarchically using sub-workflows, which run as separate orchestration instances visible in the DTS dashboard.
|
||||
|
||||
## Running the Samples
|
||||
|
||||
|
||||
@@ -565,8 +565,8 @@ internal class DurableWorkflowRunner
|
||||
/// <returns>A <see cref="WorkflowExecutorInfo"/> containing metadata about the executor.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the executor ID is not found in bindings.</exception>
|
||||
/// <remarks>
|
||||
/// This method determines the executor type (agentic vs regular) and extracts request port
|
||||
/// information for human-in-the-loop executors.
|
||||
/// This method determines the executor type (agentic, sub-workflow, request port, or regular)
|
||||
/// and extracts the appropriate metadata for each type.
|
||||
/// </remarks>
|
||||
private static WorkflowExecutorInfo CreateExecutorInfo(
|
||||
string executorId,
|
||||
@@ -579,8 +579,9 @@ internal class DurableWorkflowRunner
|
||||
|
||||
bool isAgentic = WorkflowHelper.IsAgentExecutorType(binding.ExecutorType);
|
||||
RequestPort? requestPort = (binding is RequestPortBinding rpb) ? rpb.Port : null;
|
||||
Workflow? subWorkflow = (binding is SubworkflowBinding swb) ? swb.WorkflowInstance : null;
|
||||
|
||||
return new WorkflowExecutorInfo(executorId, isAgentic, requestPort);
|
||||
return new WorkflowExecutorInfo(executorId, isAgentic, requestPort, subWorkflow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1092,6 +1093,12 @@ internal class DurableWorkflowRunner
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <strong>Sub-Workflow Executors:</strong> Executed as sub-orchestrations.
|
||||
/// The child workflow runs as a separate orchestration instance with its own instance ID.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <strong>Regular Executors:</strong> Invoked as Durable Task activities. Input is wrapped
|
||||
/// with state and type information via <see cref="ActivityInputWithState"/>.
|
||||
/// </description>
|
||||
@@ -1119,6 +1126,12 @@ internal class DurableWorkflowRunner
|
||||
return await ExecuteRequestPortAsync(context, executorInfo, input, logger, customStatus).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
// Handle Sub-Workflow executors by calling as sub-orchestrations
|
||||
if (executorInfo.IsSubworkflowExecutor)
|
||||
{
|
||||
return await ExecuteSubWorkflowAsync(context, executorInfo, input, logger).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
if (!executorInfo.IsAgenticExecutor)
|
||||
{
|
||||
string executorName = WorkflowNamingHelper.GetExecutorName(executorInfo.ExecutorId);
|
||||
@@ -1139,6 +1152,54 @@ internal class DurableWorkflowRunner
|
||||
return await ExecuteAgentAsync(context, executorInfo, input, logger).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a sub-workflow as a sub-orchestration.
|
||||
/// </summary>
|
||||
/// <param name="context">The orchestration context for calling sub-orchestrations.</param>
|
||||
/// <param name="executorInfo">The executor info containing the sub-workflow reference.</param>
|
||||
/// <param name="input">The input to pass to the sub-workflow.</param>
|
||||
/// <param name="logger">The logger for tracing.</param>
|
||||
/// <returns>The result of the sub-workflow execution.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Sub-workflows are executed as separate orchestration instances.
|
||||
/// This provides:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Separate instance ID for the child workflow (visible in dashboard)</description></item>
|
||||
/// <item><description>Independent checkpointing and replay</description></item>
|
||||
/// <item><description>Failure isolation (child failure doesn't corrupt parent state)</description></item>
|
||||
/// <item><description>Hierarchical visualization in the Durable Task dashboard</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
private static async Task<string> ExecuteSubWorkflowAsync(
|
||||
TaskOrchestrationContext context,
|
||||
WorkflowExecutorInfo executorInfo,
|
||||
string input,
|
||||
ILogger logger)
|
||||
{
|
||||
Workflow subWorkflow = executorInfo.SubWorkflow!;
|
||||
string subOrchestrationName = WorkflowNamingHelper.ToOrchestrationFunctionName(subWorkflow.Name!);
|
||||
|
||||
logger.LogDebug(
|
||||
"Calling sub-orchestration '{SubOrchestrationName}' for sub-workflow '{SubWorkflowName}'",
|
||||
subOrchestrationName,
|
||||
subWorkflow.Name);
|
||||
|
||||
// Call the sub-workflow as a sub-orchestration
|
||||
// The Durable Task Framework handles checkpointing, replay, and failure isolation
|
||||
string result = await context.CallSubOrchestratorAsync<string>(
|
||||
subOrchestrationName,
|
||||
input).ConfigureAwait(true);
|
||||
|
||||
logger.LogDebug(
|
||||
"Sub-orchestration '{SubOrchestrationName}' completed with result length: {ResultLength}",
|
||||
subOrchestrationName,
|
||||
result?.Length ?? 0);
|
||||
|
||||
return result ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a request port executor by waiting for an external event (human-in-the-loop).
|
||||
/// </summary>
|
||||
|
||||
+76
-4
@@ -49,13 +49,21 @@ public static class DurableWorkflowServiceCollectionExtensions
|
||||
// Register the workflow runner
|
||||
services.AddSingleton<DurableWorkflowRunner>();
|
||||
|
||||
// Build registration info for all workflows
|
||||
// Build registration info for all workflows (including sub-workflows)
|
||||
List<WorkflowRegistrationInfo> registrations = [];
|
||||
HashSet<string> registeredActivities = [];
|
||||
HashSet<string> registeredOrchestrations = [];
|
||||
|
||||
foreach (KeyValuePair<string, Workflow> workflowEntry in durableOptions.Workflows.Workflows)
|
||||
// Take a snapshot of the workflows to avoid collection modified during enumeration
|
||||
// (sub-workflows are added to the collection during recursive registration)
|
||||
foreach (Workflow workflow in durableOptions.Workflows.Workflows.Values.ToList())
|
||||
{
|
||||
registrations.Add(BuildWorkflowRegistration(workflowEntry.Value, registeredActivities));
|
||||
BuildWorkflowRegistrationRecursive(
|
||||
workflow,
|
||||
durableOptions.Workflows,
|
||||
registrations,
|
||||
registeredActivities,
|
||||
registeredOrchestrations);
|
||||
}
|
||||
|
||||
// Get any AI agents that were auto-registered from workflows
|
||||
@@ -121,6 +129,53 @@ public static class DurableWorkflowServiceCollectionExtensions
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively builds workflow registrations, including any sub-workflows.
|
||||
/// Also adds sub-workflows to the workflow options so they can be looked up at runtime.
|
||||
/// </summary>
|
||||
/// <param name="workflow">The workflow to register.</param>
|
||||
/// <param name="workflowOptions">The workflow options to add sub-workflows to.</param>
|
||||
/// <param name="registrations">The list to add registrations to.</param>
|
||||
/// <param name="registeredActivities">Set of already registered activity names to avoid duplicates.</param>
|
||||
/// <param name="registeredOrchestrations">Set of already registered orchestration names to avoid duplicates.</param>
|
||||
private static void BuildWorkflowRegistrationRecursive(
|
||||
Workflow workflow,
|
||||
DurableWorkflowOptions workflowOptions,
|
||||
List<WorkflowRegistrationInfo> registrations,
|
||||
HashSet<string> registeredActivities,
|
||||
HashSet<string> registeredOrchestrations)
|
||||
{
|
||||
string orchestrationName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflow.Name!);
|
||||
|
||||
// Skip if this workflow is already registered (handles circular references)
|
||||
if (!registeredOrchestrations.Add(orchestrationName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Build registration for this workflow
|
||||
registrations.Add(BuildWorkflowRegistration(workflow, registeredActivities));
|
||||
|
||||
// Recursively register any sub-workflows
|
||||
foreach (KeyValuePair<string, ExecutorBinding> entry in workflow.ReflectExecutors())
|
||||
{
|
||||
if (entry.Value is SubworkflowBinding subworkflowBinding)
|
||||
{
|
||||
Workflow subWorkflow = subworkflowBinding.WorkflowInstance;
|
||||
|
||||
// Add sub-workflow to options so it can be looked up by the runner at runtime
|
||||
workflowOptions.AddWorkflow(subWorkflow);
|
||||
|
||||
BuildWorkflowRegistrationRecursive(
|
||||
subWorkflow,
|
||||
workflowOptions,
|
||||
registrations,
|
||||
registeredActivities,
|
||||
registeredOrchestrations);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static WorkflowRegistrationInfo BuildWorkflowRegistration(
|
||||
Workflow workflow,
|
||||
HashSet<string> registeredActivities)
|
||||
@@ -139,6 +194,12 @@ public static class DurableWorkflowServiceCollectionExtensions
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip sub-workflow executors - they're handled as sub-orchestrations
|
||||
if (entry.Value is SubworkflowBinding)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string executorName = WorkflowNamingHelper.GetExecutorName(entry.Key);
|
||||
string activityName = WorkflowNamingHelper.ToOrchestrationFunctionName(executorName);
|
||||
|
||||
@@ -237,7 +298,18 @@ public static class DurableWorkflowServiceCollectionExtensions
|
||||
}
|
||||
|
||||
// Try to load the type directly (for types not in supported types)
|
||||
return Type.GetType(inputTypeName) ?? supportedTypes.FirstOrDefault() ?? typeof(string);
|
||||
Type? loadedType = Type.GetType(inputTypeName);
|
||||
|
||||
// If the loaded type is string but the executor doesn't support string,
|
||||
// fall back to the first supported type. This handles the case where
|
||||
// serialized JSON objects are passed with type "System.String" but need
|
||||
// to be deserialized to the actual expected type (e.g., OrderInfo).
|
||||
if (loadedType == typeof(string) && !supportedTypes.Contains(typeof(string)))
|
||||
{
|
||||
return supportedTypes.FirstOrDefault() ?? typeof(string);
|
||||
}
|
||||
|
||||
return loadedType ?? supportedTypes.FirstOrDefault() ?? typeof(string);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -11,13 +11,19 @@ namespace Microsoft.Agents.AI.DurableTask;
|
||||
/// <param name="ExecutorId">The unique identifier of the executor.</param>
|
||||
/// <param name="IsAgenticExecutor">Indicates whether this executor is an agentic executor.</param>
|
||||
/// <param name="RequestPort">The request port if this executor is a request port executor; otherwise, null.</param>
|
||||
[DebuggerDisplay("{ExecutorId}, Agentic = {IsAgenticExecutor}, HITL = {IsRequestPortExecutor}")]
|
||||
internal sealed record WorkflowExecutorInfo(string ExecutorId, bool IsAgenticExecutor, RequestPort? RequestPort = null)
|
||||
/// <param name="SubWorkflow">The sub-workflow if this executor is a sub-workflow executor; otherwise, null.</param>
|
||||
[DebuggerDisplay("{ExecutorId}, Agentic = {IsAgenticExecutor}, HITL = {IsRequestPortExecutor}, SubWorkflow = {IsSubworkflowExecutor}")]
|
||||
internal sealed record WorkflowExecutorInfo(string ExecutorId, bool IsAgenticExecutor, RequestPort? RequestPort = null, Workflow? SubWorkflow = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this executor is a request port executor (human-in-the-loop).
|
||||
/// </summary>
|
||||
public bool IsRequestPortExecutor => this.RequestPort is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this executor is a sub-workflow executor.
|
||||
/// </summary>
|
||||
public bool IsSubworkflowExecutor => this.SubWorkflow is not null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -176,7 +182,8 @@ internal static class WorkflowHelper
|
||||
ExecutorBinding executorBinding = executors[executorId];
|
||||
bool isAgentic = IsAgentExecutorType(executorBinding.ExecutorType);
|
||||
RequestPort? requestPort = (executorBinding is RequestPortBinding rpb) ? rpb.Port : null;
|
||||
levelExecutors.Add(new WorkflowExecutorInfo(executorId, isAgentic, requestPort));
|
||||
Workflow? subWorkflow = (executorBinding is SubworkflowBinding swb) ? swb.WorkflowInstance : null;
|
||||
levelExecutors.Add(new WorkflowExecutorInfo(executorId, isAgentic, requestPort, subWorkflow));
|
||||
|
||||
// Check Fan-In for this executor (excluding back-edges)
|
||||
int nonBackEdgePredecessors = predecessors[executorId]
|
||||
@@ -218,7 +225,8 @@ internal static class WorkflowHelper
|
||||
{
|
||||
bool isAgentic = IsAgentExecutorType(executor.Value.ExecutorType);
|
||||
RequestPort? requestPort = (executor.Value is RequestPortBinding rpb) ? rpb.Port : null;
|
||||
remainingExecutors.Add(new WorkflowExecutorInfo(executor.Key, isAgentic, requestPort));
|
||||
Workflow? subWorkflow = (executor.Value is SubworkflowBinding swb) ? swb.WorkflowInstance : null;
|
||||
remainingExecutors.Add(new WorkflowExecutorInfo(executor.Key, isAgentic, requestPort, subWorkflow));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user