Skip to content

.NET: [Feature Branch] Add Human In the Loop support for durable workflows#4358

Open
kshyju wants to merge 10 commits intofeat/durable_taskfrom
shkr/feat_durable_task_hitl
Open

.NET: [Feature Branch] Add Human In the Loop support for durable workflows#4358
kshyju wants to merge 10 commits intofeat/durable_taskfrom
shkr/feat_durable_task_hitl

Conversation

@kshyju
Copy link
Contributor

@kshyju kshyju commented Feb 27, 2026

Motivation and Context

This PR introduces Human-in-the-Loop (HITL) support for durable workflows. It allows workflows to pause at defined checkpoints and wait for external input, enabling scenarios such as approval processes, human reviews, and other interactive decision-making workflows.

Note: This PR targets the feat/durable_task feature branch.

Key scenarios enabled:

  • Pausing workflow execution at a RequestPort to wait for external input (e.g., manager approval)
  • Streaming DurableWorkflowWaitingForInputEvent to callers so they can discover what input is needed
  • Resuming workflows by sending typed responses via SendResponseAsync or HTTP endpoints
  • Multiple sequential HITL pause points in a single workflow (e.g., manager approval → finance approval)
  • Auto-generated HTTP endpoints for HITL responses (and optional status queries) in Azure Functions

Description

This PR introduces RequestPort-based HITL support for durable workflows, with both a streaming client API and auto-generated HTTP endpoints for Azure Functions hosting.

New Public APIs

API Description
IStreamingWorkflowRun.SendResponseAsync Sends a typed response to a DurableWorkflowWaitingForInputEvent to resume the workflow.
DurableWorkflowWaitingForInputEvent Event yielded when the workflow is paused at a RequestPort. Contains the serialized input and the RequestPort definition.
DurableWorkflowWaitingForInputEvent.GetInputAs<T>() Deserializes the request input to a typed object.
DurableWorkflowOptionsExtensions.AddWorkflow(workflow, exposeStatusEndpoint) Extension method (Azure Functions) to register a workflow with an optional status endpoint.
// Define a workflow with a RequestPort (MAF core Workflow API — not new in this PR)
CreateApprovalRequest createRequest = new();
RequestPort<ApprovalRequest, ApprovalResponse> managerApproval =
    RequestPort.Create<ApprovalRequest, ApprovalResponse>("ManagerApproval");
ExpenseReimburse reimburse = new();

// Build the workflow: CreateApprovalRequest -> ManagerApproval (HITL) -> ExpenseReimburse
Workflow expenseApproval = new WorkflowBuilder(createRequest)
    .WithName("ExpenseReimbursement")
    .AddEdge(createRequest, managerApproval)
    .AddEdge(managerApproval, reimburse)
    .Build();
// Start a workflow with streaming and handle HITL pauses
IStreamingWorkflowRun run = await workflowClient.StreamAsync(workflow, "EXP-2025-001");

await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
    switch (evt)
    {
        case DurableWorkflowWaitingForInputEvent requestEvent:
            ApprovalRequest? request = requestEvent.GetInputAs<ApprovalRequest>();
            await run.SendResponseAsync(requestEvent, new ApprovalResponse(Approved: true));
            break;
        case DurableWorkflowCompletedEvent e:
            Console.WriteLine($"Completed: {e.Result}");
            break;
    }
}

Azure Functions Auto-Generated Endpoints

When a workflow contains RequestPort nodes, the framework auto-generates a respond endpoint. A status endpoint is also available via opt-in.

Method Endpoint Description
POST /api/workflows/{name}/respond/{runId} Send a response to resume the workflow (auto-generated when workflow has RequestPort nodes)
GET /api/workflows/{name}/status/{runId} Query workflow status and pending HITL requests (opt-in via exposeStatusEndpoint: true)
// Azure Functions: register workflow with status endpoint enabled
builder.ConfigureDurableWorkflows(workflows =>
    workflows.AddWorkflow(expenseApproval, exposeStatusEndpoint: true));

Internal Infrastructure Changes

Component Description
DurableExecutorDispatcher New ExecuteRequestPortAsync method that publishes pending request to DurableWorkflowLiveStatus, waits for external event via WaitForExternalEvent, and cleans up on response.
DurableWorkflowLiveStatus Holds both streaming events and a PendingEvents list for HITL signaling. Includes a shared TryParse method for deserialization.
DurableStreamingWorkflowRun Monitors PendingEvents in the durable workflow status and yields DurableWorkflowWaitingForInputEvent for each new pending request. Implements SendResponseAsync via RaiseEventAsync.
DurableWorkflowRunner Carries DurableWorkflowLiveStatus on SuperstepState for consistent status updates across dispatch and result processing.
ServiceCollectionExtensions Excludes RequestPortBinding from activity registration (handled as external events).
FunctionsDurableOptions Extends DurableOptions with Azure Functions–specific configuration (status endpoint opt-in).
BuiltInFunctions New GetWorkflowStatusAsync and RespondToWorkflowAsync HTTP handlers with input validation.
BuiltInFunctionExecutor Dispatch cases for the new HITL HTTP entry points.
DurableWorkflowsFunctionMetadataTransformer Auto-registers status (opt-in) and respond (when RequestPort nodes present) HTTP triggers.
FunctionMetadataFactory Added methods parameter to CreateHttpTrigger for GET endpoints.

How HITL Works

When the workflow reaches a RequestPort executor, the orchestration:

  1. Adds a PendingRequestPortStatus entry to DurableWorkflowLiveStatus.PendingEvents
  2. Publishes the status via SetCustomStatus so external clients can discover pending requests
  3. Waits on WaitForExternalEvent until the response arrives
  4. Removes the pending entry and continues execution

External actors respond via:

  • Streaming API: Call run.SendResponseAsync(requestEvent, response) after receiving a DurableWorkflowWaitingForInputEvent
  • HTTP API: POST to /api/workflows/{name}/respond/{runId} with {"eventName": "...", "response": {...}}

Validation/Testing

Samples Added:

Sample Description
08_WorkflowHITL (Console) Demonstrates two sequential HITL approval points (manager → finance) with streaming event observation. No Azure OpenAI required.
03_WorkflowHITL (Azure Functions) Demonstrates HITL with auto-generated HTTP endpoints for status queries and responses. Includes demo.http for testing. No Azure OpenAI required.

Integration Tests Added:

Test Description
WorkflowHITLSampleValidationAsync (Console) Validates the two-step HITL flow: manager approval → finance approval → completion.
HITLWorkflowSampleValidationAsync (Azure Functions) Validates the HTTP-based HITL flow: start workflow → wait for pause → send approval → verify completion.

Both samples have been manually verified against the local DTS emulator.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • Is this a breaking change? No

Add 06_WorkflowHITL Azure Functions sample demonstrating Human-in-the-Loop
workflow support with HTTP endpoints for status checking and approval responses.

The sample includes:
- ExpenseReimbursement workflow with RequestPort for manager approval
- Custom HTTP endpoint to check workflow status and pending approvals
- Custom HTTP endpoint to send approval responses via RaiseEventAsync
- demo.http file with step-by-step interaction examples
@kshyju kshyju requested a review from Copilot February 27, 2026 19:20
@markwallace-microsoft markwallace-microsoft added documentation Improvements or additions to documentation .NET labels Feb 27, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Human-in-the-Loop (HITL) support to durable workflows by pausing at RequestPort nodes, surfacing “waiting for input” signals via streaming/custom status, and enabling resumption via typed responses (streaming API and Azure Functions HTTP endpoints).

Changes:

  • Introduces RequestPort-backed “wait for external input” execution in the DurableTask workflow runner/dispatcher and exposes it via a new DurableWorkflowWaitingForInputEvent + IStreamingWorkflowRun.SendResponseAsync.
  • Adds Azure Functions auto-generated HTTP endpoints for respond (when RequestPorts exist) and optional status (opt-in per workflow).
  • Adds console + Azure Functions samples and integration tests validating HITL flows.

Reviewed changes

Copilot reviewed 33 out of 33 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs Adds an integration test that drives the Azure Functions HITL sample via run/respond endpoints and log-based assertions.
dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/WorkflowConsoleAppSamplesValidation.cs Adds an integration test that validates the console HITL sample’s sequential approval flow.
dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Workflows/DurableWorkflowsFunctionMetadataTransformer.cs Auto-registers HTTP triggers for status (opt-in) and respond (when RequestPorts exist) per workflow.
dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Workflows/DurableWorkflowOptionsExtensions.cs Adds Azure Functions–specific workflow registration overload (exposeStatusEndpoint).
dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsDurableOptions.cs Introduces Functions-specific DurableOptions subclass to track per-workflow status-endpoint enablement.
dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs Ensures shared options instance is the Functions-specific subtype; wires middleware/transformer for new endpoints.
dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionMetadataFactory.cs Extends HTTP trigger metadata creation to support configurable HTTP methods (e.g., GET for status).
dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs Adds HTTP handlers for workflow status and responding to pending RequestPorts.
dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs Routes Azure Functions invocations to the new built-in status/respond handlers.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/PendingRequestPortStatus.cs Defines the serialized durable custom-status representation of a pending RequestPort wait.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/IStreamingWorkflowRun.cs Adds SendResponseAsync API to resume a workflow after a waiting-for-input event.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowWaitingForInputEvent.cs Introduces the streaming event emitted when a workflow is paused at a RequestPort; includes typed input deserialization helper.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowRunner.cs Publishes a richer live status payload (events + pending RequestPorts) into custom status for streaming/polling.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowOptions.cs Exposes parent options via ParentOptions (used for cross-feature coordination).
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowLiveStatus.cs Adds new live-status payload type that includes streamed events + pending RequestPorts and parsing helper.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowJsonContext.cs Updates source-gen JSON context registrations for new live-status/pending-request types.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowCustomStatus.cs Removes the prior events-only custom status type (replaced by live status payload).
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableStreamingWorkflowRun.cs Streams both normal workflow events and new waiting-for-input events; implements SendResponseAsync.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableExecutorDispatcher.cs Adds RequestPort dispatch path that writes pending status + waits for external events.
dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs Excludes RequestPort binding from activity registration (handled via external events instead).
dotnet/src/Microsoft.Agents.AI.DurableTask/Logs.cs Adds log messages for waiting/received external RequestPort events used by tests and diagnostics.
dotnet/src/Microsoft.Agents.AI.DurableTask/DurableOptions.cs Makes DurableOptions non-sealed to enable hosting-specific extension.
dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowHITL/README.md Documents the console HITL sample usage and expected output.
dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowHITL/Program.cs Adds the console sample demonstrating sequential RequestPort pauses and streaming resumption.
dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowHITL/Executors.cs Adds sample executors and request/response types for the console HITL scenario.
dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowHITL/08_WorkflowHITL.csproj Adds the console HITL sample project.
dotnet/samples/Durable/Workflow/AzureFunctions/03_WorkflowHITL/host.json Adds host configuration for local DTS emulator + logging for the Functions HITL sample.
dotnet/samples/Durable/Workflow/AzureFunctions/03_WorkflowHITL/demo.http Adds a runnable HTTP script showing run/status/respond flow.
dotnet/samples/Durable/Workflow/AzureFunctions/03_WorkflowHITL/README.md Documents the Azure Functions HITL sample endpoints and usage.
dotnet/samples/Durable/Workflow/AzureFunctions/03_WorkflowHITL/Program.cs Adds the Azure Functions sample wiring a workflow with exposeStatusEndpoint: true.
dotnet/samples/Durable/Workflow/AzureFunctions/03_WorkflowHITL/Executors.cs Adds sample executors and request/response types for the Functions HITL scenario.
dotnet/samples/Durable/Workflow/AzureFunctions/03_WorkflowHITL/03_WorkflowHITL.csproj Adds the Azure Functions HITL sample project.
dotnet/agent-framework-dotnet.slnx Registers the new console and Azure Functions HITL samples in the solution.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 34 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Workflows/DurableWorkflowsFunctionMetadataTransformer.cs:145

  • RequestPort executors are dispatched via external events (not Durable Task activities), but this metadata transformer currently treats any non-agent/non-subworkflow binding as an activity trigger. This will register an activity function for RequestPortBinding executors even though they’re never called and aren’t registered as activities in Microsoft.Agents.AI.DurableTask. Consider skipping RequestPortBinding here (similar to SubworkflowBinding) to avoid generating unused activity triggers and potential confusion.

…gEvents.Add`/`RemoveAll` and `SetCustomStatus` in `ExecuteRequestPortAsync`. The guards broke fan-out scenarios where parallel RequestPorts need to be discoverable after replay. `SetCustomStatus` is idempotent metadata that doesn't affect replay determinism.eanup
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 34 changed files in this pull request and generated 2 comments.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 34 changed files in this pull request and generated 1 comment.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 34 changed files in this pull request and generated no new comments.

Copy link
Member

@cgillum cgillum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few initial comments about the samples. Will look at the implementation next.

Copy link
Member

@cgillum cgillum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to sign off for the day, but a couple other small things for now.

HashSet<string> registeredFunctions = [];

foreach (var workflow in this._options.Workflows)
foreach (var workflow in this._options.Workflows.Workflows)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking nit: this looks weird. I'd expect something like this._options.WorkflowOptions.Workflows.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation .NET

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants