Skip to content

Python: Durable Task multi-workflow hosting and sub-workflows#6696

Draft
ahmedmuhsin wants to merge 7 commits into
microsoft:mainfrom
ahmedmuhsin:feature/python-durabletask-subworkflows-multiworkflow
Draft

Python: Durable Task multi-workflow hosting and sub-workflows#6696
ahmedmuhsin wants to merge 7 commits into
microsoft:mainfrom
ahmedmuhsin:feature/python-durabletask-subworkflows-multiworkflow

Conversation

@ahmedmuhsin

Copy link
Copy Markdown
Contributor

Motivation & Context

The Durable Task hosting layer only ran a single workflow per worker or function app. It registered one fixed orchestrator name and named each executor's durable activity or entity by the bare executor id. Two workflows could not be co-hosted, and a workflow could not embed another workflow as a node. This change runs multiple workflows per host and composes workflows from nested sub-workflows. It brings the durable execution model in line with the in-process MAF workflow model and aligns the orchestration name with the .NET durable host.

Description & Review Guide

  • What are the major changes?

Multiple workflows per host. DurableAIAgentWorker.configure_workflow and AgentFunctionApp(workflows=...) register any number of workflows. Each workflow gets its own dafx-{name} orchestration, and on the function app its own workflow/{name}/run, status, and respond routes. The orchestration name matches the .NET durable host.

Per-workflow durable names. Inner activities and agent entities are scoped as dafx-{workflow}-{executor} so two co-hosted workflows that reuse an executor id resolve to distinct primitives instead of shadowing each other. Agent conversation state stays isolated by the entity key, which is still the orchestration instance id.

Sub-workflows as child orchestrations. A WorkflowExecutor node runs its inner workflow as a durable child orchestration. Both hosts walk the composition and register every reachable workflow once, deduped by name. The shared engine and both host context adapters gained a call_sub_orchestrator primitive.

Nested human-in-the-loop behind one surface. A request_info pause inside a sub-workflow is recorded on the child instance. The parent records its child instance ids in custom status, and the read side bubbles nested pending requests up to the top-level instance with a qualified request id of the form {executor}~{ordinal}~{requestId}, nested deeper for deeper levels. The caller always talks to the top-level run, and the host routes the response to the owning child. The ~{ordinal}~ hop keeps each child of a fan-out node independently addressable. The separator is ~ rather than : so it never collides with the framework's own auto::N functional-workflow request ids.

Workflow and executor identity validation. Workflow names must be explicit and stable. Auto generated WorkflowBuilder-{uuid} names are rejected because they change on every build and would break durable resume. Two different workflow instances that share a name are rejected, while the same instance reused across nodes is deduped. Executor ids are validated for durable hosting so they stay free of the reserved separator and within the durable name length limit.

Trust boundary for the sub-orchestration envelope. The envelope carries the parent serialized child payload and is reconstructed with pickle on the trusted side. A real envelope is only ever built internally after the trust boundary, so both hosts strip the reserved envelope key from untrusted client input before scheduling a run. A forged envelope cannot reach the trusted deserialization path.

Docs and samples. A new ADR and design document capture the multi-workflow and sub-workflow decisions. New durabletask samples cover sub-workflow composition and nested sub-workflow HITL, a new no-agent Azure Functions sample covers sub-workflow HITL, and the existing function app samples move to the per-workflow route shape.

  • What is the impact of these changes?

The single-workflow hosting path now uses the per-workflow dafx-{name} orchestration name and the workflow/{name}/... routes. WORKFLOW_ORCHESTRATOR_NAME stays exported as a deprecated source alias and is no longer used for dispatch. An orchestration started under the old fixed name will not resume against the new name, so this lands before any rollout that relies on resume across the upgrade. New public helpers are exported for hosts and clients, including workflow_orchestrator_name, validate_workflow_name, validate_executor_id, collect_hosted_workflows, and the request id qualification helpers.

Sub-workflow nesting is not capped by a depth counter. A WorkflowExecutor wraps a concrete Workflow, so the nesting tree is finite at build time and the durable instance id length limit is the natural ceiling. This matches the .NET host, which imposes no limit.

  • What do you want reviewers to focus on?

The nested HITL addressing and routing across both hosts, the trust boundary that strips the sub-workflow envelope key from untrusted input, and the per-workflow naming and validation that keeps two co-hosted workflows from colliding.

Related Issue

There is no separate tracking issue. This extends the Durable Task workflow hosting that already exists in the repository with multi-workflow and sub-workflow support.

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.

Foundation for hosting multiple workflows (and later sub-workflows) on one
durable task host. Adds a host-agnostic naming module that derives the stable
durable names a hosted workflow registers under.

- New `_workflows/naming.py`:
  - `workflow_orchestrator_name(name)` -> `dafx-{name}` (orchestration name,
    aligned byte-for-byte with .NET `WorkflowNamingHelper`).
  - `workflow_name_from_orchestrator(name)` -> reverse, `None` when not prefixed.
  - `validate_workflow_name(name)` -> rejects empty / malformed / auto-generated
    `WorkflowBuilder-<uuid>` names (validate-and-reject rather than silently
    sanitize, since the name becomes a durable identity and an HTTP route segment).
  - `is_auto_generated_workflow_name(name)`, `DURABLE_NAME_PREFIX`.
- Export the helpers from the package public API.
- Mark `WORKFLOW_ORCHESTRATOR_NAME` deprecated in favor of per-workflow names
  (kept functional; the single-workflow path still uses it until phase 1).
- 39 unit tests covering round-trips and validation.

Design: docs/design/durabletask-multiworkflow-and-subworkflows.md
…es (phase 1)

Enables hosting more than one MAF workflow on a single standalone Durable Task
worker, and aligns both hosts on workflow-scoped durable names so two co-hosted
workflows that reuse an executor id cannot collide.

Naming (shared, host-agnostic):
- orchestration: dafx-{workflowName} (matches .NET; the name DT tooling surfaces)
- non-agent activity / agent entity: dafx-{workflowName}-{executorId} (scoped)
- New naming helpers workflow_scoped_executor_id / workflow_executor_activity_name.

Standalone worker (agent-framework-durabletask):
- configure_workflow is now additive: stores workflows keyed by Workflow.name,
  rejects duplicate / auto-generated (WorkflowBuilder-<uuid>) / invalid names,
  registers one orchestrator per workflow plus its scoped activities/entities.
- The shared orchestrator dispatches scoped names derived from workflow.name.
- New registered_workflow_names property.

Client (DurableWorkflowClient):
- Optional default workflow_name on the client; start/run/stream accept a per-call
  workflow_name and target dafx-{name}.
- Opt-in ownership validation on status/HITL methods: when a workflow name is
  resolvable, an instance whose orchestration name does not match is treated as
  not-found (status -> None, pending -> [], send_hitl_response / await -> raise),
  mirroring the Azure Functions route-scoping check.

Azure Functions host (agent-framework-azurefunctions):
- Registration now uses the same scoped names so the shared orchestrator's
  dispatch matches (single workflow per app for now; flat workflow/* routes kept).
- Workflow name is validated up front; workflow agents register under the scoped
  entity id; _is_workflow_orchestration scopes to dafx-{workflow.name}.

Samples + tests:
- Durable Task and Azure Functions workflow samples now name their workflow.
- Unit tests cover multi-workflow registration, name validation, client targeting,
  and ownership; integration tests target the named workflows.

WORKFLOW_ORCHESTRATOR_NAME remains exported (deprecated). This is a hard switch:
in-flight single-workflow instances created before upgrade (under the old
workflow_orchestrator name) will not resume.

Design: docs/design/durabletask-multiworkflow-and-subworkflows.md
…ow routes (phase 2)

Completes multi-workflow hosting on the Azure Functions host, building on the
shared scoped-naming foundation from the worker phase.

AgentFunctionApp:
- New `workflows=` parameter accepting a list (keyed by each `Workflow.name`) or a
  name->Workflow mapping; the existing `workflow=` is a single-workflow alias.
  Both may be combined. Duplicate names and mapping-key/name mismatches are rejected.
- Each workflow registers its own `dafx-{name}` orchestration, workflow-scoped
  activities/entities, and per-workflow HTTP routes:
  `workflow/{name}/run`, `workflow/{name}/status/{instanceId}`,
  `workflow/{name}/respond/{instanceId}/{requestId}`. Routes are always
  per-workflow (even for a single workflow) so callers don't change URLs as an app
  grows from one workflow to many.
- Route ownership check is per-workflow (`_is_owned_orchestration(status, name)`):
  a leaked instance id for another orchestration -- or another workflow -- is
  treated as not-found, extending the route-scoping defense.
- `get_agent(context, name, workflow_name=...)` resolves a workflow agent under its
  scoped id; bare `agents=` registration keeps the standalone surface. New
  `workflows` introspection property; `.workflow` now returns the sole workflow
  (or None when several are hosted).
- Removed the now-unused flat-URL helper `_build_status_url` (handlers inline
  per-workflow URLs).

Samples + tests:
- Azure Functions workflow samples (09-12) name their workflow; integration tests
  target the per-workflow routes.
- Unit tests cover multi-workflow registration, duplicate/mapping/auto-name
  rejection, and per-workflow ownership.

Note: sample README / demo.http route docs are updated in the docs phase.

Design: docs/design/durabletask-multiworkflow-and-subworkflows.md
…ase 3)

Run WorkflowExecutor nodes as durable child orchestrations on both hosts.

- Protocol: add call_sub_orchestrator to WorkflowOrchestrationContext, implemented by the durabletask and Azure Functions adapters.

- Registration: planner classifies WorkflowExecutor as subworkflow_executors; collect_hosted_workflows walks nested workflows (parent first, deduped by name). Both hosts recursively register every nested workflow's orchestration/agents/activities once; only top-level workflows get HTTP routes. Names validated up front before any registration side effects.

- Orchestrator: dispatch WorkflowExecutor nodes via call_sub_orchestrator(dafx-{innerName}) with deterministic child instance ids ({instanceId}::{executorId}::{counter}), a trusted-input marker carrying nesting depth (bounded at 25), and outputs routed as messages (default) or parent outputs (allow_direct_output).

- Tests: registration/collect, orchestrator prepare/process/unwrap, recursive registration on both hosts. Sample: 11_subworkflow.
Surface a nested sub-workflow's human-in-the-loop request behind the top-level instance (B2 single addressing surface).

- Orchestrator records dispatched sub-workflow child instance ids in its custom status (subworkflows map) before suspending in task_all, so the read side can reach a child's pending request while the parent is paused.

- Read side (durabletask client get_pending_hitl_requests; AF status route) recurses into nested child statuses, qualifying each nested request id as {executorId}::{requestId} (accumulated for deeper nesting).

- Write side (durabletask client send_hitl_response; AF respond route) splits a qualified id on '::', resolves the owning child orchestration via the parent's subworkflows map, and raises the event on the leaf child with the bare request id. Unknown/inactive sub-workflow -> error/404.

- Shared SUBWORKFLOW_REQUEST_SEPARATOR ('::') in naming so both hosts and the client agree. respondUrl/respond always targets the top-level instance.

- Tests: TestSubworkflowHitl (durabletask client, 7), TestAgentFunctionAppSubworkflowHitl (AF, 7). Sample: 12_subworkflow_hitl (HITL pause inside an embedded sub-workflow).
…-workflows (phase 5)

- Add ADR-0030 capturing the multi-workflow and sub-workflow hosting decisions (naming, scoped inner names, per-workflow routes, child-orchestration sub-workflows, hard-switch migration, B2 sub-workflow HITL, scoped agent addressing) with considered alternatives; mark the design doc as implemented and link the ADR.

- Update Azure Functions workflow samples (09-12) README/demo.http to the per-workflow route shape (workflow/{name}/run|status|respond) introduced in phase 2.

- Extend the durabletask sample catalog with the workflow hosting patterns (08-12), including the new 11_subworkflow and 12_subworkflow_hitl samples.
…gration tests

Post-review hardening of the multi-workflow / sub-workflow durable hosting:

- Trust boundary: strip the reserved sub-workflow envelope key from untrusted
  client input at both host boundaries (DurableWorkflowClient.start_workflow and
  the AF start route) so a forged envelope cannot reach the trusted pickle path.
- Nested HITL addressing: qualify nested pending requests by (executorId, ordinal)
  using a '~' separator (was '::', which collided with core's auto::N functional
  request ids); the parent status subworkflows map is now a per-executor list so
  multiple children dispatched in one superstep stay independently addressable.
- Reject two different workflow instances that share a name (the same instance
  reused by sibling nodes is still deduped); validate executor ids (separator-free,
  length-bounded) when hosting durably.
- Remove the arbitrary sub-workflow nesting depth cap: a WorkflowExecutor wraps a
  concrete Workflow so the nesting tree is finite at build time, and the durable
  instance-id length limit is the natural ceiling (matches .NET, which has none).

Tests/samples:
- New durabletask integration tests for sub-workflow composition (11) and nested
  sub-workflow HITL (12); new no-agent AF sub-workflow HITL sample (13) + test.
- Exempt no-agent samples from the model-credential gate in both integration
  conftests so the nested-HITL plumbing is covered deterministically.
- Update durabletask sample 12 docs to the new qualified-id format.

Validated: 484 unit tests; durabletask integration 08/09/11/12 and AF 12/13 pass
against the live emulators; pyright 0 errors; ruff clean.
Copilot AI review requested due to automatic review settings June 24, 2026 01:14
@moonbox3 moonbox3 added documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python labels Jun 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/azurefunctions/agent_framework_azurefunctions
   _app.py58715773%81–82, 85–88, 316, 358–359, 364, 406–407, 411–412, 414–416, 424–426, 428, 431, 441–442, 444–446, 448, 452, 455, 457, 459, 462, 481–483, 486–489, 495, 497, 499–500, 502, 520–522, 524, 530–531, 538, 540, 555–560, 573, 575, 590–591, 593–594, 599–601, 603–606, 610, 615–618, 622, 628, 681, 718, 724, 727, 730, 735–738, 882–883, 991, 999–1000, 1020–1022, 1028–1030, 1036–1038, 1071–1072, 1132–1133, 1182–1183, 1188, 1270, 1273, 1282–1284, 1286–1288, 1290, 1292, 1303–1306, 1308, 1310–1311, 1313, 1320–1321, 1323–1324, 1326–1327, 1329, 1333, 1343–1345, 1347–1348, 1350–1352, 1359, 1361–1362, 1364, 1385, 1390, 1402, 1474, 1564, 1579–1582, 1607
   _workflow_af_context.py572850%27, 35–36, 40–41, 50, 54–55, 60–64, 67–68, 71–72, 77, 80, 85, 88, 93, 96–97, 100–102, 105
packages/durabletask/agent_framework_durabletask
   _worker.py1181785%251–252, 257, 312, 329–330, 332–334, 371, 373, 378, 393, 397–398, 402–403
packages/durabletask/agent_framework_durabletask/_workflows
   client.py1641491%178, 315, 321–322, 336, 389, 414, 417, 506, 509–510, 512, 518, 521
   context.py240100% 
   dt_context.py562457%34–35, 41, 45, 52, 56, 61–64, 67, 70, 75, 78, 83, 86, 91, 94, 98–100, 103–105
   naming.py52198%294
   orchestrator.py42227135%210–212, 264–266, 283, 289–291, 323–324, 326–333, 335, 342, 357, 359–368, 370–371, 373, 434–435, 437–438, 440–443, 446–448, 450–451, 453–458, 460–463, 465, 467–471, 480–482, 484, 486–487, 489–494, 496–497, 499–500, 502–503, 505, 518–522, 529, 542, 549–551, 553–554, 556, 574–575, 596, 651, 655, 681, 683–685, 687–688, 690, 692–693, 699, 701, 708, 712, 717, 723–724, 726–728, 730–732, 734–741, 743–744, 777–779, 781, 783–786, 788–791, 796–801, 811–812, 815–816, 825–827, 829–832, 841, 843, 881, 884–885, 889, 902, 904–907, 909, 912–920, 922, 929–931, 935–938, 944–946, 948, 952, 954–956, 959, 966–970, 973–975, 983–990, 992–1000, 1002–1004, 1007–1011, 1013–1015, 1018–1019, 1022–1023, 1026, 1028, 1033–1034, 1037–1038, 1040, 1054, 1060–1061, 1063–1064, 1071–1076, 1082–1084, 1089, 1091–1092, 1097, 1099, 1101, 1105–1106, 1108
   registration.py360100% 
   serialization.py98495%223, 338–340
TOTAL42477502388% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
8429 37 💤 0 ❌ 0 🔥 2m 6s ⏱️

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

Adds multi-workflow hosting and sub-workflow composition to the Python Durable Task hosting stack (standalone DurableAIAgentWorker, Azure Functions AgentFunctionApp, and DurableWorkflowClient) by introducing per-workflow durable naming (dafx-{workflow}), scoping inner durable primitives by workflow, and executing WorkflowExecutor nodes as child orchestrations with nested HITL request propagation/routing.

Changes:

  • Introduces a shared durable naming/validation layer for stable per-workflow orchestration + scoped executor identities, and updates worker/client/hosts to use it.
  • Executes WorkflowExecutor nodes as durable child orchestrations, including bubbling nested HITL requests to the top-level instance via qualified request ids and routing responses back down.
  • Updates samples, unit/integration tests, and adds an ADR documenting the decisions and tradeoffs.

Reviewed changes

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

Show a summary per file
File Description
python/samples/04-hosting/durabletask/README.md Adds workflow-hosting sample catalog entries (including sub-workflows/HITL).
python/samples/04-hosting/durabletask/12_subworkflow_hitl/worker.py New standalone worker sample: nested HITL pause inside a sub-workflow.
python/samples/04-hosting/durabletask/12_subworkflow_hitl/README.md Documentation for the durabletask nested sub-workflow HITL sample.
python/samples/04-hosting/durabletask/12_subworkflow_hitl/client.py New client sample driving nested HITL via qualified request ids.
python/samples/04-hosting/durabletask/11_subworkflow/worker.py New standalone worker sample: composed workflow via child orchestration.
python/samples/04-hosting/durabletask/11_subworkflow/README.md Documentation for the durabletask sub-workflow composition sample.
python/samples/04-hosting/durabletask/11_subworkflow/client.py New client sample for the composed (outer+inner) workflow.
python/samples/04-hosting/durabletask/09_workflow_hitl/worker.py Ensures workflow has a stable explicit name for durable hosting.
python/samples/04-hosting/durabletask/09_workflow_hitl/client.py Updates client to target the named workflow orchestration.
python/samples/04-hosting/durabletask/08_workflow/worker.py Ensures workflow has a stable explicit name for durable hosting.
python/samples/04-hosting/durabletask/08_workflow/client.py Updates client to target the named workflow orchestration.
python/samples/04-hosting/azure_functions/13_subworkflow_hitl/requirements.txt New Azure Functions sample deps (local editable installs).
python/samples/04-hosting/azure_functions/13_subworkflow_hitl/README.md New Azure Functions sample docs for nested sub-workflow HITL.
python/samples/04-hosting/azure_functions/13_subworkflow_hitl/local.settings.json.sample New Azure Functions sample local settings template.
python/samples/04-hosting/azure_functions/13_subworkflow_hitl/host.json New Azure Functions sample host configuration.
python/samples/04-hosting/azure_functions/13_subworkflow_hitl/function_app.py New Azure Functions sample implementing nested sub-workflow HITL.
python/samples/04-hosting/azure_functions/13_subworkflow_hitl/demo.http New REST-client demo for nested HITL routes and qualified ids.
python/samples/04-hosting/azure_functions/13_subworkflow_hitl/.gitignore Ignores local settings/venv for the new sample.
python/samples/04-hosting/azure_functions/12_workflow_hitl/README.md Updates docs to per-workflow route shape.
python/samples/04-hosting/azure_functions/12_workflow_hitl/function_app.py Names the workflow so routes/orchestrator are per-workflow.
python/samples/04-hosting/azure_functions/12_workflow_hitl/demo.http Updates demo URLs to per-workflow route shape.
python/samples/04-hosting/azure_functions/11_workflow_parallel/README.md Updates docs to per-workflow route shape.
python/samples/04-hosting/azure_functions/11_workflow_parallel/function_app.py Names the workflow so routes/orchestrator are per-workflow.
python/samples/04-hosting/azure_functions/11_workflow_parallel/demo.http Updates demo URLs to per-workflow route shape.
python/samples/04-hosting/azure_functions/10_workflow_no_shared_state/README.md Updates docs to per-workflow route shape.
python/samples/04-hosting/azure_functions/10_workflow_no_shared_state/function_app.py Names the workflow so routes/orchestrator are per-workflow.
python/samples/04-hosting/azure_functions/10_workflow_no_shared_state/demo.http Updates demo URLs to per-workflow route shape.
python/samples/04-hosting/azure_functions/09_workflow_shared_state/README.md Updates docs to per-workflow route shape.
python/samples/04-hosting/azure_functions/09_workflow_shared_state/function_app.py Names the workflow so routes/orchestrator are per-workflow.
python/samples/04-hosting/azure_functions/09_workflow_shared_state/demo.http Updates demo URLs to per-workflow route shape.
python/packages/durabletask/tests/test_workflow_serialization.py Adds tests for stripping forged sub-workflow envelope markers.
python/packages/durabletask/tests/test_workflow_registration.py Adds sub-workflow classification + recursive hosted-workflow collection tests.
python/packages/durabletask/tests/test_workflow_naming.py New tests for per-workflow naming/validation + qualified request id helpers.
python/packages/durabletask/tests/test_workflow_client.py Updates client tests for per-workflow orchestration targeting + nested HITL routing.
python/packages/durabletask/tests/test_worker.py Updates worker tests for scoped names, multi-workflow registration, and sub-workflow registration.
python/packages/durabletask/tests/test_subworkflow_orchestration.py New tests for child-orchestration dispatch and sub-workflow input/result handling.
python/packages/durabletask/tests/integration_tests/test_12_dt_subworkflow_hitl.py New integration test: nested HITL in durabletask standalone host.
python/packages/durabletask/tests/integration_tests/test_11_dt_subworkflow.py New integration test: sub-workflow composition on durabletask standalone host.
python/packages/durabletask/tests/integration_tests/test_09_dt_workflow_hitl.py Updates integration test to target named per-workflow orchestration.
python/packages/durabletask/tests/integration_tests/test_08_dt_workflow.py Updates integration test to schedule dafx-{workflow} orchestration.
python/packages/durabletask/tests/integration_tests/conftest.py Relaxes env gating for no-LLM samples.
python/packages/durabletask/agent_framework_durabletask/_workflows/serialization.py Adds reserved sub-workflow envelope key + boundary stripping helper.
python/packages/durabletask/agent_framework_durabletask/_workflows/registration.py Adds WorkflowExecutor planning and recursive hosted-workflow collection.
python/packages/durabletask/agent_framework_durabletask/_workflows/orchestrator.py Implements child-orchestration execution for sub-workflows + status surfacing for nested HITL.
python/packages/durabletask/agent_framework_durabletask/_workflows/naming.py New canonical durable naming + validation + qualified request id helpers.
python/packages/durabletask/agent_framework_durabletask/_workflows/dt_context.py Adds call_sub_orchestrator support to the durabletask context adapter.
python/packages/durabletask/agent_framework_durabletask/_workflows/context.py Extends context interface to support child orchestration calls.
python/packages/durabletask/agent_framework_durabletask/_workflows/client.py Adds per-workflow targeting, ownership validation, nested HITL gather/routing, and input hardening.
python/packages/durabletask/agent_framework_durabletask/_worker.py Enables multi-workflow hosting, scoped durable names, sub-workflow recursion, and validations.
python/packages/durabletask/agent_framework_durabletask/init.py Exports new naming/validation/collection helpers.
python/packages/azurefunctions/tests/test_app.py Updates Azure Functions host tests for multi-workflow, scoping, and nested HITL plumbing.
python/packages/azurefunctions/tests/integration_tests/test_13_workflow_subworkflow_hitl.py New integration test for nested sub-workflow HITL via Functions routes.
python/packages/azurefunctions/tests/integration_tests/test_12_workflow_hitl.py Updates integration test routes to per-workflow shape.
python/packages/azurefunctions/tests/integration_tests/test_11_workflow_parallel.py Updates integration test routes to per-workflow shape.
python/packages/azurefunctions/tests/integration_tests/test_10_workflow_no_shared_state.py Updates integration test routes to per-workflow shape.
python/packages/azurefunctions/tests/integration_tests/test_09_workflow_shared_state.py Updates integration test routes to per-workflow shape.
python/packages/azurefunctions/tests/integration_tests/conftest.py Relaxes env gating for no-LLM Functions sample.
python/packages/azurefunctions/agent_framework_azurefunctions/_workflow_af_context.py Adds call_sub_orchestrator support to the Azure Functions context adapter.
docs/decisions/0030-durabletask-multiworkflow-and-subworkflows.md ADR capturing the multi-workflow + sub-workflow durable hosting decisions.

- **[09_workflow_hitl](09_workflow_hitl/)**: A workflow that pauses for human approval using `ctx.request_info` / `@response_handler`, with the client discovering and answering the pending request.
- **[10_workflow_streaming](10_workflow_streaming/)**: Stream a hosted workflow's events as typed `WorkflowEvent` objects by polling the orchestration's custom status.
- **[11_subworkflow](11_subworkflow/)**: Compose workflows by embedding an inner `Workflow` as a node via `WorkflowExecutor`. On the durable host the inner workflow runs as its own child orchestration, and a single `configure_workflow` call registers both.
- **[12_subworkflow_hitl](12_subworkflow_hitl/)**: A human-in-the-loop pause that lives **inside a sub-workflow**. The nested request surfaces to the client with a qualified request id (`{executor}::{requestId}`) behind a single top-level addressing surface.
Comment on lines +8 to +9
Unlike sample 12, this sample hosts **no AI agents**, so it needs only Azurite and
the Durable Task Scheduler emulator — no model credentials.

@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: 4 | Confidence: 91%

✓ Correctness

The implementation is well-structured: trust-boundary stripping handles both dict and string inputs, closure captures for per-workflow routes are correct, recursive HITL resolution is bounded by request-id string length, and decorator ordering follows Azure Functions v2 conventions. No high-severity correctness issues found. One minor docstring inaccuracy noted. The naming, registration, sub-workflow HITL addressing, and trust boundary stripping logic are all correct. Test coverage is thorough — qualified request id round-trips, deeply nested routing, auto::N leaf ids, ownership validation, and envelope stripping are all well-tested. Two tests in test_workflow_naming.py are placed in the wrong test class (TestSubworkflowRequestIdQualification instead of TestWorkflowNameRoundTrip) but this is a minor organizational issue with no functional impact. No correctness bugs found. The code in the samples looks correct. There is one documentation bug: the parent durabletask README.md describes the qualified request id format as {executor}:{requestId} (using :: separator, two parts) when every other reference in the PR — tests, sample docstrings, the sub-sample README, and the PR description itself — consistently uses {executor}~{ordinal}~{requestId} (using ~ separator, three parts). The PR rationale explicitly states the separator is ~ rather than : to avoid colliding with the framework's own auto::N request ids.

✓ Test Coverage

Test coverage for this PR is comprehensive and well-structured. New naming helpers, registration logic, sub-workflow orchestration, and HITL request qualification all have dedicated unit tests. The Azure Functions app tests cover multi-workflow registration, duplicate/invalid name rejection, sub-workflow nesting, scoped orchestration ownership, and nested HITL gather/resolve. A new integration test (test_13_subworkflow_hitl) exercises the end-to-end nested HITL flow. Two minor gaps: (1) no unit test verifies the trust boundary integration (that strip_subworkflow_markers is called on untrusted HTTP input before scheduling), and (2) the AzureFunctionsWorkflowContext.call_sub_orchestrator adapter method has no dedicated unit test. Neither gap is blocking given the function-level tests and integration coverage already in place. Test coverage for this PR is strong overall, with dedicated test files for naming, serialization, registration, client HITL, and sub-workflow orchestration. However, there are a few notable gaps: (1) await_workflow_output and stream_workflow have no tests for the new _is_owned_orchestration ownership validation path, even though get_runtime_status, get_pending_hitl_requests, and send_hitl_response do; (2) workflow_scoped_executor_id and workflow_executor_activity_name in naming.py have zero direct unit tests; (3) the subworkflow_counter deterministic instance-id derivation in the orchestrator has no dedicated test verifying counter persistence across supersteps. Test coverage for this PR is extensive and well-structured. New unit tests cover naming/validation, registration (including sub-workflows), orchestration dispatch, serialization trust boundaries, and the client-side sub-workflow HITL qualified-request-id scheme. Integration tests cover both the standalone durabletask worker and Azure Functions hosts. Two minor gaps: (1) the TestOwnershipValidation class tests foreign-instance rejection for get_runtime_status, get_pending_hitl_requests, and send_hitl_response, but omits await_workflow_output which has the same ownership check (client.py:177-178) with no unit-level coverage of that ValueError path; (2) two tests in TestSubworkflowRequestIdQualification actually test workflow_name_from_orchestrator rather than request-id qualification, which is a minor organizational issue. Test coverage is thorough: unit tests exist for naming/validation (test_workflow_naming.py), sub-workflow orchestration (test_subworkflow_orchestration.py), and integration tests for both the standalone DT worker (test_11_dt_subworkflow.py, test_12_dt_subworkflow_hitl.py) and Azure Functions (test_13_workflow_subworkflow_hitl.py) hosting paths. One documentation bug was found: the README uses the wrong separator character in the qualified request id format.

✓ Failure Modes

The diff is well-structured with thorough validation and trust-boundary handling. One concrete failure mode: in configure_workflow, the top-level self._workflows dict is mutated before the registration loop that can raise ValueError on a cross-call sub-workflow name collision. This leaves the worker in an unrecoverable inconsistent state (the name is 'registered' but primitives are not, and retrying is blocked by the duplicate check). The recursive HITL resolution and naming logic are sound, and the sub-workflow envelope trust boundary is properly enforced on both hosts. The :: separator collides with the framework's internal auto::N namespace (which is why ~ was chosen), and the two-part format omits the ordinal hop that keeps fan-out children independently addressable. A user copying this format to parse or build request ids would produce ids that fail to route. The sample code and sample-level README are correct.

✓ Design Approach

The Azure Functions multi-workflow registration currently leaves a cross-workflow boundary gap when two workflow names differ only by case. Registration accepts both names as distinct, but the status/respond ownership check later compares orchestration names case-insensitively, so one workflow’s routes can read or inject events into the other’s instances. The sub-workflow hosting design is mostly coherent, but one behavioral gap remains: child workflows do not propagate their event stream back to the parent durable workflow, so nested intermediate emissions disappear from stream_workflow() even though the in-process WorkflowExecutor forwards them through the parent surface. I found one design-level inconsistency in the new workflow naming contract: it now explicitly allows mixed-case workflow names, but the runtime’s ownership checks treat orchestration names case-insensitively while registration stores them case-sensitively. That means two co-hosted workflows whose names differ only by case can still collide at the status/HITL boundary, undermining the PR’s isolation goal. I found one nonblocking design/documentation mismatch in the new durabletask sample catalog. The new 12_subworkflow_hitl entry documents the nested HITL request-id shape as {executor}::{requestId}, but the shipped contract and tests in this PR use ~-qualified ids such as review_sub~0~{requestId}. That inconsistency would send readers toward an addressing scheme the client/host do not accept.

Suggestions

  • Add an ownership-validation unit test for await_workflow_output (foreign instance raises ValueError), matching the pattern already established in TestOwnershipValidation for the other client methods. The ValueError path at client.py:177-178 currently has zero unit coverage.

Automated review by ahmedmuhsin's agents

same name is already registered.
"""
validate_workflow_name(workflow.name)
if workflow.name in self._workflows:

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.

This duplicate check is case-sensitive, but _is_owned_orchestration() later authorizes status/respond by comparing status.name.casefold() to workflow_orchestrator_name(workflow_name).casefold() (lines 752-757). That means hosting Orders and orders succeeds here, yet either workflow route can operate on the other's instances because both orchestration names collapse to the same case-folded value. Please reject workflow names case-insensitively (and do the same for _registered_orchestrations) so the route boundary stays real.

class TestValidateWorkflowName:
"""``validate_workflow_name`` rejects unstable / unsafe identities."""

@pytest.mark.parametrize("name", ["a", "A", "wf", "Order_Processor", "spam-detection", "x" * 63])

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.

Allowing mixed-case workflow names here bakes in a contract the rest of the change does not consistently honor. Registration keeps raw names as distinct keys, so orders and Orders can both be hosted. But both ownership guards compare the orchestration name with casefold(), so the orders client/route will also accept an instance of Orders and can read its status or inject a HITL response. The naming contract should reject case-insensitive collisions (or normalize names to one case) instead of treating case variants as valid distinct identities.

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