-
Notifications
You must be signed in to change notification settings - Fork 1.2k
.NET: [Feature Branch] Add Human In the Loop support for durable workflows #4358
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
kshyju
wants to merge
10
commits into
feat/durable_task
Choose a base branch
from
shkr/feat_durable_task_hitl
base: feat/durable_task
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,896
−107
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
84148a1
Add Azure Functions HITL workflow sample
kshyju dc8a881
PR feedback fixes
kshyju 1179b4d
Minor comment cleanup
kshyju 719c525
Minor comment clReverted the `!context.IsReplaying` guards on `Pendin…
kshyju 927f172
fix for PR feedback
kshyju 1946f03
PR feedback updates
kshyju 5dfbe3e
Improvements to samples
kshyju 3d27a7a
Improvements to README
kshyju f7ae1ce
Update samples to use parallel request ports.
kshyju 9274da4
Unit tests
kshyju File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 43 additions & 0 deletions
43
dotnet/samples/Durable/Workflow/AzureFunctions/03_WorkflowHITL/03_WorkflowHITL.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| <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>WorkflowHITLFunctions</AssemblyName> | ||
| <RootNamespace>WorkflowHITLFunctions</RootNamespace> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <FrameworkReference Include="Microsoft.AspNetCore.App" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <None Include="local.settings.json" /> | ||
| </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> |
63 changes: 63 additions & 0 deletions
63
dotnet/samples/Durable/Workflow/AzureFunctions/03_WorkflowHITL/Executors.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| using Microsoft.Agents.AI.Workflows; | ||
|
|
||
| namespace WorkflowHITLFunctions; | ||
|
|
||
| /// <summary>Expense approval request passed to the RequestPort.</summary> | ||
| public record ApprovalRequest(string ExpenseId, decimal Amount, string EmployeeName); | ||
|
|
||
| /// <summary>Approval response received from the RequestPort.</summary> | ||
| public record ApprovalResponse(bool Approved, string? Comments); | ||
|
|
||
| /// <summary>Looks up expense details and creates an approval request.</summary> | ||
| internal sealed class CreateApprovalRequest() : Executor<string, ApprovalRequest>("RetrieveRequest") | ||
| { | ||
| public override ValueTask<ApprovalRequest> HandleAsync( | ||
| string message, | ||
| IWorkflowContext context, | ||
| CancellationToken cancellationToken = default) | ||
| { | ||
| // In a real scenario, this would look up expense details from a database | ||
| return new ValueTask<ApprovalRequest>(new ApprovalRequest(message, 1500.00m, "Jerry")); | ||
| } | ||
| } | ||
|
|
||
| /// <summary>Prepares the approval request for finance review after manager approval.</summary> | ||
| internal sealed class PrepareFinanceReview() : Executor<ApprovalResponse, ApprovalRequest>("PrepareFinanceReview") | ||
| { | ||
| public override ValueTask<ApprovalRequest> HandleAsync( | ||
| ApprovalResponse message, | ||
| IWorkflowContext context, | ||
| CancellationToken cancellationToken = default) | ||
| { | ||
| if (!message.Approved) | ||
| { | ||
| throw new InvalidOperationException("Cannot proceed to finance review — manager denied the expense."); | ||
| } | ||
|
|
||
| // In a real scenario, this would retrieve the original expense details | ||
| return new ValueTask<ApprovalRequest>(new ApprovalRequest("EXP-2025-001", 1500.00m, "Jerry")); | ||
| } | ||
| } | ||
|
|
||
| /// <summary>Processes the expense reimbursement based on the parallel approval responses.</summary> | ||
| internal sealed class ExpenseReimburse() : Executor<ApprovalResponse[], string>("Reimburse") | ||
| { | ||
| public override async ValueTask<string> HandleAsync( | ||
| ApprovalResponse[] message, | ||
| IWorkflowContext context, | ||
| CancellationToken cancellationToken = default) | ||
| { | ||
| // Check that all parallel approvals passed | ||
| ApprovalResponse? denied = Array.Find(message, r => !r.Approved); | ||
| if (denied is not null) | ||
| { | ||
| return $"Expense reimbursement denied. Comments: {denied.Comments}"; | ||
| } | ||
|
|
||
| // Simulate payment processing | ||
| await Task.Delay(1000, cancellationToken); | ||
| return $"Expense reimbursed at {DateTime.UtcNow:O}"; | ||
| } | ||
| } |
51 changes: 51 additions & 0 deletions
51
dotnet/samples/Durable/Workflow/AzureFunctions/03_WorkflowHITL/Program.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| // This sample demonstrates a Human-in-the-Loop (HITL) workflow hosted in Azure Functions. | ||
| // | ||
| // ┌──────────────────────┐ ┌────────────────┐ ┌─────────────────────┐ ┌────────────────────┐ | ||
| // │ CreateApprovalRequest│──►│ManagerApproval │──►│PrepareFinanceReview │──┬►│ BudgetApproval │──┐ | ||
| // └──────────────────────┘ │ (RequestPort) │ └─────────────────────┘ │ │ (RequestPort) │ │ | ||
| // └────────────────┘ │ └────────────────────┘ │ ┌─────────────────┐ | ||
| // │ ├─►│ExpenseReimburse │ | ||
| // │ ┌────────────────────┐ │ └─────────────────┘ | ||
| // └►│ComplianceApproval │──┘ | ||
| // │ (RequestPort) │ | ||
| // └────────────────────┘ | ||
| // | ||
| // The workflow pauses at three RequestPorts — one for the manager, then two in parallel for finance. | ||
| // After manager approval, BudgetApproval and ComplianceApproval run concurrently via fan-out/fan-in. | ||
| // The framework auto-generates three HTTP endpoints for each workflow: | ||
| // POST /api/workflows/{name}/run - Start the workflow | ||
| // GET /api/workflows/{name}/status/{id} - Check status and pending approvals | ||
| // POST /api/workflows/{name}/respond/{id} - Send approval response to resume | ||
|
|
||
| using Microsoft.Agents.AI.Hosting.AzureFunctions; | ||
| using Microsoft.Agents.AI.Workflows; | ||
| using Microsoft.Azure.Functions.Worker.Builder; | ||
| using Microsoft.Extensions.Hosting; | ||
| using WorkflowHITLFunctions; | ||
|
|
||
| // Define executors and RequestPorts for the three HITL pause points | ||
| CreateApprovalRequest createRequest = new(); | ||
| RequestPort<ApprovalRequest, ApprovalResponse> managerApproval = RequestPort.Create<ApprovalRequest, ApprovalResponse>("ManagerApproval"); | ||
| PrepareFinanceReview prepareFinanceReview = new(); | ||
| RequestPort<ApprovalRequest, ApprovalResponse> budgetApproval = RequestPort.Create<ApprovalRequest, ApprovalResponse>("BudgetApproval"); | ||
| RequestPort<ApprovalRequest, ApprovalResponse> complianceApproval = RequestPort.Create<ApprovalRequest, ApprovalResponse>("ComplianceApproval"); | ||
| ExpenseReimburse reimburse = new(); | ||
|
|
||
| // Build the workflow: CreateApprovalRequest -> ManagerApproval -> PrepareFinanceReview -> [BudgetApproval AND ComplianceApproval] -> ExpenseReimburse | ||
| Workflow expenseApproval = new WorkflowBuilder(createRequest) | ||
| .WithName("ExpenseReimbursement") | ||
| .WithDescription("Expense reimbursement with manager and parallel finance approvals") | ||
| .AddEdge(createRequest, managerApproval) | ||
| .AddEdge(managerApproval, prepareFinanceReview) | ||
| .AddFanOutEdge(prepareFinanceReview, [budgetApproval, complianceApproval]) | ||
| .AddFanInEdge([budgetApproval, complianceApproval], reimburse) | ||
| .Build(); | ||
|
|
||
| using IHost app = FunctionsApplication | ||
| .CreateBuilder(args) | ||
| .ConfigureFunctionsWebApplication() | ||
| .ConfigureDurableWorkflows(workflows => workflows.AddWorkflow(expenseApproval, exposeStatusEndpoint: true)) | ||
| .Build(); | ||
| app.Run(); |
196 changes: 196 additions & 0 deletions
196
dotnet/samples/Durable/Workflow/AzureFunctions/03_WorkflowHITL/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| # Human-in-the-Loop (HITL) Workflow — Azure Functions | ||
|
|
||
| This sample demonstrates a durable workflow with Human-in-the-Loop support hosted in Azure Functions. The workflow pauses at three `RequestPort` nodes — one sequential manager approval, then two parallel finance approvals (budget and compliance) via fan-out/fan-in. Approval responses are sent via HTTP endpoints. | ||
|
|
||
| ## Key Concepts Demonstrated | ||
|
|
||
| - Using multiple `RequestPort` nodes for sequential and parallel human-in-the-loop interactions in a durable workflow | ||
| - Fan-out/fan-in pattern for parallel approval steps | ||
| - Auto-generated HTTP endpoints for running workflows, checking status, and sending HITL responses | ||
| - Pausing orchestrations via `WaitForExternalEvent` and resuming via `RaiseEventAsync` | ||
| - Viewing inputs the workflow is waiting for via the status endpoint | ||
|
|
||
| ## Workflow | ||
|
|
||
| This sample implements the following workflow: | ||
|
|
||
| ``` | ||
| ┌──────────────────────┐ ┌────────────────┐ ┌─────────────────────┐ ┌────────────────────┐ | ||
| │ CreateApprovalRequest│──►│ManagerApproval │──►│PrepareFinanceReview │──┬►│ BudgetApproval │──┐ | ||
| └──────────────────────┘ │ (RequestPort) │ └─────────────────────┘ │ │ (RequestPort) │ │ | ||
| └────────────────┘ │ └────────────────────┘ │ ┌─────────────────┐ | ||
| │ ├─►│ExpenseReimburse │ | ||
| │ ┌────────────────────┐ │ └─────────────────┘ | ||
| └►│ComplianceApproval │──┘ | ||
| │ (RequestPort) │ | ||
| └────────────────────┘ | ||
| ``` | ||
|
|
||
| ## HTTP Endpoints | ||
|
|
||
| The framework auto-generates these endpoints for workflows with `RequestPort` nodes: | ||
|
|
||
| | Method | Endpoint | Description | | ||
| |--------|----------|-------------| | ||
| | POST | `/api/workflows/ExpenseReimbursement/run` | Start the workflow | | ||
| | GET | `/api/workflows/ExpenseReimbursement/status/{runId}` | Check status and inputs the workflow is waiting for | | ||
| | POST | `/api/workflows/ExpenseReimbursement/respond/{runId}` | Send approval response to resume | | ||
|
|
||
| ## Environment Setup | ||
|
|
||
| See the [README.md](../../README.md) file in the parent directory for information on how to configure the environment, including how to install and run the Durable Task Scheduler. | ||
|
|
||
| ## Running the Sample | ||
|
|
||
| With the environment setup and function app running, you can test the sample by sending HTTP requests to the workflow endpoints. | ||
|
|
||
| You can use the `demo.http` file to trigger the workflow, or a command line tool like `curl` as shown below: | ||
|
|
||
| ### Step 1: Start the Workflow | ||
|
|
||
| Bash (Linux/macOS/WSL): | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:7071/api/workflows/ExpenseReimbursement/run \ | ||
| -H "Content-Type: text/plain" -d "EXP-2025-001" | ||
| ``` | ||
|
|
||
| PowerShell: | ||
|
|
||
| ```powershell | ||
| Invoke-RestMethod -Method Post ` | ||
| -Uri http://localhost:7071/api/workflows/ExpenseReimbursement/run ` | ||
| -ContentType text/plain ` | ||
| -Body "EXP-2025-001" | ||
| ``` | ||
|
|
||
| The response will confirm the workflow orchestration has started: | ||
|
|
||
| ```text | ||
| Workflow orchestration started for ExpenseReimbursement. Orchestration runId: abc123def456 | ||
| ``` | ||
|
|
||
| > **Tip:** You can provide a custom run ID by appending a `runId` query parameter: | ||
| > | ||
| > ```bash | ||
| > curl -X POST "http://localhost:7071/api/workflows/ExpenseReimbursement/run?runId=expense-001" \ | ||
| > -H "Content-Type: text/plain" -d "EXP-2025-001" | ||
| > ``` | ||
| > | ||
| > If not provided, a unique run ID is auto-generated. | ||
|
|
||
| ### Step 2: Check Workflow Status | ||
|
|
||
| The workflow pauses at the `ManagerApproval` RequestPort. Query the status endpoint to see what input it is waiting for: | ||
|
|
||
| ```bash | ||
| curl http://localhost:7071/api/workflows/ExpenseReimbursement/status/{runId} | ||
| ``` | ||
|
|
||
| ```json | ||
| { | ||
| "runId": "{runId}", | ||
| "status": "Running", | ||
| "waitingForInput": [ | ||
| { "eventName": "ManagerApproval", "input": { "ExpenseId": "EXP-2025-001", "Amount": 1500.00, "EmployeeName": "Jerry" } } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| > **Tip:** You can also verify this in the DTS dashboard at `http://localhost:8082`. Find the orchestration by its `runId` and you will see it is in a "Running" state, paused at a `WaitForExternalEvent` call for the `ManagerApproval` event. | ||
|
|
||
| ### Step 3: Send Manager Approval Response | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:7071/api/workflows/ExpenseReimbursement/respond/{runId} \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"eventName": "ManagerApproval", "response": {"Approved": true, "Comments": "Approved by manager."}}' | ||
| ``` | ||
|
|
||
| ```json | ||
| { | ||
| "message": "Response sent to workflow.", | ||
| "runId": "{runId}", | ||
| "eventName": "ManagerApproval", | ||
| "validated": true | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 4: Check Workflow Status Again | ||
|
|
||
| The workflow now pauses at both the `BudgetApproval` and `ComplianceApproval` RequestPorts in parallel: | ||
|
|
||
| ```bash | ||
| curl http://localhost:7071/api/workflows/ExpenseReimbursement/status/{runId} | ||
| ``` | ||
|
|
||
| ```json | ||
| { | ||
| "runId": "{runId}", | ||
| "status": "Running", | ||
| "waitingForInput": [ | ||
| { "eventName": "BudgetApproval", "input": { "ExpenseId": "EXP-2025-001", "Amount": 1500.00, "EmployeeName": "Jerry" } }, | ||
| { "eventName": "ComplianceApproval", "input": { "ExpenseId": "EXP-2025-001", "Amount": 1500.00, "EmployeeName": "Jerry" } } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 5a: Send Budget Approval Response | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:7071/api/workflows/ExpenseReimbursement/respond/{runId} \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"eventName": "BudgetApproval", "response": {"Approved": true, "Comments": "Budget approved."}}' | ||
| ``` | ||
|
|
||
| ```json | ||
| { | ||
| "message": "Response sent to workflow.", | ||
| "runId": "{runId}", | ||
| "eventName": "BudgetApproval", | ||
| "validated": true | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 5b: Send Compliance Approval Response | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:7071/api/workflows/ExpenseReimbursement/respond/{runId} \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"eventName": "ComplianceApproval", "response": {"Approved": true, "Comments": "Compliance approved."}}' | ||
| ``` | ||
|
|
||
| ```json | ||
| { | ||
| "message": "Response sent to workflow.", | ||
| "runId": "{runId}", | ||
| "eventName": "ComplianceApproval", | ||
| "validated": true | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 6: Check Final Status | ||
|
|
||
| After all approvals, the workflow completes and the expense is reimbursed: | ||
|
|
||
| ```bash | ||
| curl http://localhost:7071/api/workflows/ExpenseReimbursement/status/{runId} | ||
| ``` | ||
|
|
||
| ```json | ||
| { | ||
| "runId": "{runId}", | ||
| "status": "Completed", | ||
| "waitingForInput": null | ||
| } | ||
| ``` | ||
|
|
||
| ### Viewing Workflows in the DTS Dashboard | ||
|
|
||
| After running a workflow, you can navigate to the Durable Task Scheduler (DTS) dashboard to visualize the orchestration and inspect its execution history. | ||
|
|
||
| If you are using the DTS emulator, the dashboard is available at `http://localhost:8082`. | ||
|
|
||
| 1. Open the dashboard and look for the orchestration instance matching the `runId` returned in Step 1 (e.g., `abc123def456` or your custom ID like `expense-001`). | ||
| 2. Click into the instance to see the execution timeline, which shows each executor activity and the `WaitForExternalEvent` pauses where the workflow waited for human input — including the two parallel finance approvals. | ||
| 3. Expand individual activity steps to inspect inputs and outputs — for example, the `ManagerApproval`, `BudgetApproval`, and `ComplianceApproval` external events will show the approval request sent and the response received. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.