Skip to content

Python: HITL respond-URL addressing from inside workflows#7001

Draft
ahmedmuhsin wants to merge 4 commits into
microsoft:mainfrom
ahmedmuhsin:feature/python-durabletask-workflow-hitl-context
Draft

Python: HITL respond-URL addressing from inside workflows#7001
ahmedmuhsin wants to merge 4 commits into
microsoft:mainfrom
ahmedmuhsin:feature/python-durabletask-workflow-hitl-context

Conversation

@ahmedmuhsin

Copy link
Copy Markdown
Contributor

Motivation & Context

The durable function HITL flow pauses a workflow with request_info and waits for a human to POST a response to the /respond endpoint. The gap was that nothing inside the workflow could build that respond URL. An executor only receives its message payload, not the orchestration instanceId, and the requestId does not exist until request_info runs. So to email someone an approval link you had to pull both ids from the HTTP run response or poll the status endpoint from outside the app.

This came out of a real POC where someone wanted Agent B to email a reviewer when it finished, then resume to Agent C once approved, but had no way to address the running workflow from within it. This change lets a workflow build its own respond URL and notify a human from inside the graph with no caller plumbing of instanceId or requestId, and it works for nested sub workflows too.

Description & Review Guide

  • What are the major changes?

    • The orchestrator now surfaces orchestration metadata (the addressable root instanceId, the workflow name, and for nested workflows the request path prefix) onto each executor through the durable runner context. There is no new core API.
    • A new WorkflowHitlContext helper in agent-framework-azurefunctions builds the canonical respond and status URLs from that metadata. It returns None in process so the same executor degrades gracefully when it is not on the durable host.
    • WorkflowHitlContext.pending_request_id(ctx) reads the id request_info just generated back off the context, so the user never mints an id by hand. It works on any host through the existing core runner context protocol.
    • For nested sub workflows the address context propagates down call_sub_orchestrator via a new SUBWORKFLOW_ADDRESS_KEY marker, so an executor at any depth builds a URL that targets the top level instance with a qualified request id. That marker is stripped from untrusted input alongside the existing input marker, so a top level caller cannot forge it.
    • Samples 12 and 13 show the two step pattern. The review executor reads the request id back and sends it to a downstream notify executor that builds the URL, so a retried activity never emails a dead link because only the committed attempt reaches the notifier.
  • What is the impact of these changes?

    • Additive. No core change, nothing public removed, and top level behavior is unchanged when the helper is not used.
    • New unit tests cover the metadata round trip, the id read back, the nested ordinal and prefix agreement with the read side, marker stripping, and URL building. Integration tests assert that the URL the helper builds equals the server respondUrl and that posting to it resumes the run, for both the flat (12) and nested (13) samples.
  • What do you want reviewers to focus on?

    • The per executor ordinal used to build the nested prefix at dispatch has to match the enumerate index the status and respond endpoints use when they resolve a nested request. That agreement is the one spot where a mistake would send an emailed URL to the wrong child or a 404.

Related Issue

No separate issue. This builds on the Durable Task multi-workflow and sub-workflow hosting work that landed on main in #6696, and now targets main directly.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

…ecutors

Let a workflow notify a human reviewer (e.g. email an approval link) from inside the
graph, without the caller threading the instanceId/requestId by hand.

- durabletask: the orchestrator injects host_context {instance_id, workflow_name,
  request_path_prefix} into each activity input; CapturingRunnerContext surfaces it as
  host_metadata. No new core API.
- azurefunctions: WorkflowHitlContext.from_context(ctx) builds the canonical
  respond/status URLs (returns None in-process so callers degrade gracefully).
  Re-exported through the agent_framework.azure lazy namespace.
- Nested sub-workflows: the address context (root instance + workflow name +
  accumulated {executor}~{ordinal}~ prefix) propagates down call_sub_orchestrator via a
  new SUBWORKFLOW_ADDRESS_KEY marker, so an executor at any depth builds a URL that
  targets the addressable top-level instance with a qualified request id. The per-child
  ordinal matches the read-side enumerate() index used by the status/respond endpoints.
  The marker is stripped from untrusted input alongside SUBWORKFLOW_INPUT_KEY
  (confused-deputy / info-leak guard).
- Samples 12 and 13 reworked into the retry-safe two-step notify pattern: the emitter
  generates an explicit request id and a downstream NotifyExecutor builds the URL and
  notifies, so failed upstream retries never produce a dead link.

Tests: unit coverage for the metadata round-trip, address/ordinal agreement (fan-out at
depth and nested prefix accumulation), marker stripping, and URL building; integration
tests assert the helper-built URL equals the server respondUrl and resumes the run, for
both the flat (12) and nested (13) samples.
…g one in samples

Add WorkflowHitlContext.pending_request_id(ctx), an async helper that returns the
id request_info just generated (read from the runner context's pending request-info
events). This works on any host via the core RunnerContext protocol method, so it
needs no core change.

Samples 12 and 13 now call request_info() and read the id back to forward to the
NotifyExecutor, instead of minting a uuid by hand and passing request_id=. The
read-back happens in the same activity execution that generated the id, so the
pending request event and the notify message still commit together with the same id
(retry-safe; failed upstream retries notify no one).
…afety

Tighten pending_request_id docstring to require calling it immediately after request_info, and explain why that is safe on the durable host (each executor runs in its own activity with its own runner context, so the pending set only holds this executor's requests and the newest is the one just emitted). Document the two-step notify pattern in the 12 and 13 sample READMEs, including the downstream-notifier retry safety and the nested address-prefix propagation.
Copilot AI review requested due to automatic review settings July 8, 2026 21:59
@giles17 giles17 added documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python labels Jul 8, 2026
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/azurefunctions/agent_framework_azurefunctions
   _hitl_context.py52198%170
packages/durabletask/agent_framework_durabletask/_workflows
   activity.py741974%62, 85–91, 109–111, 149–153, 159, 172–173
   orchestrator.py45325743%204–206, 258–260, 278, 295–297, 336–337, 339–346, 348, 355, 370, 372–381, 383–384, 386, 483–484, 486–487, 489–492, 495–497, 499–500, 502–507, 509–512, 514, 516–520, 529–531, 533, 535–536, 538–543, 545–546, 548–549, 551–552, 554, 567–571, 578, 591, 598–600, 602–603, 605, 623–624, 645, 740, 744, 770, 772–774, 776–777, 779, 781–782, 788, 790, 797, 801, 806, 812–813, 815–817, 819–821, 823–830, 832–833, 892, 924–925, 928–929, 939–940, 942–945, 954, 998, 1001–1002, 1009, 1016, 1020, 1033, 1035–1038, 1040, 1043–1051, 1053, 1060–1062, 1066–1069, 1075–1077, 1079, 1083, 1085–1087, 1090, 1097–1101, 1104–1106, 1114–1121, 1123–1130, 1134–1135, 1137–1139, 1142–1146, 1148–1150, 1153–1154, 1157–1158, 1161, 1163, 1168–1169, 1172–1173, 1175, 1189, 1195–1196, 1198–1199, 1206–1211, 1217–1219, 1224, 1226–1227, 1232, 1234, 1236, 1240–1241, 1245–1247
   runner_context.py782370%49–50, 58, 63, 75, 78, 83, 86, 89, 100, 103, 106, 111, 114–117, 120, 123, 152, 157–158, 161
   serialization.py101496%243, 358–360
TOTAL44172525788% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
8856 33 💤 0 ❌ 0 🔥 2m 24s ⏱️

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated Code Review

Reviewers: 5 | Confidence: 91% | Result: All clear

Reviewed: Correctness, Security Reliability, Test Coverage, Failure Modes, Design Approach


Automated review by ahmedmuhsin's agents

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR enables Python Durable Functions workflows (including nested sub-workflows) to construct canonical HITL /respond (and /status) URLs from inside executors, by surfacing orchestration addressing metadata onto the runner context and adding a small helper (WorkflowHitlContext) in agent-framework-azurefunctions.

Changes:

  • Surface durable-host orchestration metadata (root instance/workflow + nested request-path prefix) to executors via durabletask runner context host_metadata.
  • Add WorkflowHitlContext helper to build respond/status URLs and to read back the latest request_info-generated request id from the runner context.
  • Update samples + add unit/integration tests, including nested sub-workflow address propagation and marker-stripping security coverage.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
python/samples/04-hosting/azure_functions/13_subworkflow_hitl/README.md Documents in-subworkflow notification pattern using WorkflowHitlContext.
python/samples/04-hosting/azure_functions/13_subworkflow_hitl/function_app.py Adds inner-workflow notifier executor and wires it into the nested HITL sample.
python/samples/04-hosting/azure_functions/12_workflow_hitl/README.md Documents in-workflow reviewer notification + respond URL building.
python/samples/04-hosting/azure_functions/12_workflow_hitl/function_app.py Adds notifier executor and routes approval result explicitly to publish step.
python/packages/durabletask/tests/test_workflow_serialization.py Adds tests ensuring subworkflow address marker is stripped from untrusted input.
python/packages/durabletask/tests/test_workflow_activity.py Adds tests asserting host_context is surfaced as runner_context.host_metadata.
python/packages/durabletask/tests/test_subworkflow_orchestration.py Adds tests for address resolution + per-superstep subworkflow ordinal/prefix agreement.
python/packages/durabletask/agent_framework_durabletask/_workflows/serialization.py Introduces SUBWORKFLOW_ADDRESS_KEY and strips it alongside SUBWORKFLOW_INPUT_KEY.
python/packages/durabletask/agent_framework_durabletask/_workflows/runner_context.py Adds host_metadata plumbing to the durable activity runner context.
python/packages/durabletask/agent_framework_durabletask/_workflows/orchestrator.py Threads address context into activity/subworkflow dispatch and propagates nested prefix.
python/packages/durabletask/agent_framework_durabletask/_workflows/activity.py Reads host_context and sets it on the runner context for executor access.
python/packages/core/agent_framework/azure/init.pyi Re-exports WorkflowHitlContext from the agent_framework.azure facade typings.
python/packages/core/agent_framework/azure/init.py Adds lazy export mapping for WorkflowHitlContext.
python/packages/azurefunctions/tests/test_hitl_context.py New unit tests for WorkflowHitlContext URL building/base URL resolution/pending id.
python/packages/azurefunctions/tests/integration_tests/test_13_workflow_subworkflow_hitl.py Integration test asserts helper-built URL matches server qualified respondUrl (nested).
python/packages/azurefunctions/tests/integration_tests/test_12_workflow_hitl.py Integration test asserts helper-built URL matches server respondUrl (flat).
python/packages/azurefunctions/agent_framework_azurefunctions/_hitl_context.py New helper implementation for HITL URL construction + pending request id read-back.
python/packages/azurefunctions/agent_framework_azurefunctions/init.py Exports WorkflowHitlContext from the package public surface.

Comment thread python/samples/04-hosting/azure_functions/12_workflow_hitl/README.md Outdated
Comment thread python/samples/04-hosting/azure_functions/13_subworkflow_hitl/README.md Outdated
Comment thread python/packages/azurefunctions/tests/integration_tests/test_12_workflow_hitl.py Outdated
- test_subworkflow_orchestration: replace the mypy-only type:ignore[arg-type] with a cast so the ty checker passes too (the other four checkers already honored the ignore).

- samples 12/13 README: guard the notify snippet against None before build_respond_url to match the documented graceful-degradation behavior.

- integration tests 12/13: reword comments that implied request_info now generates an explicit uuid4; it generates the id internally by default.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants