Sub-Orchestration¶
Trigger: Durable Orchestration | State: durable | Guarantee: at-least-once | Difficulty: intermediate
Overview¶
This recipe shows how a parent Durable Functions orchestrator can delegate parts of a workflow
to child sub-orchestrators with context.call_sub_orchestrator(...).
The parent stays focused on high-level coordination while each child owns its own durable
history, retries, and activity scheduling.
This is useful when one orchestration would otherwise become too large or when a repeated workflow segment deserves its own reusable orchestration boundary. Sub-orchestrations still follow the same replay-safe rules as any other orchestrator: they only coordinate durable work and do not perform direct I/O themselves.
When to Use¶
- You want to decompose a large orchestration into smaller reusable workflow units.
- Different stages of the workflow need their own durable history and status boundaries.
- The parent workflow needs to invoke nested orchestration logic in a deterministic way.
When NOT to Use¶
- A direct activity call is enough and you do not need another orchestration boundary.
- The workflow is so small that extra orchestration layers only add complexity.
- The child logic performs only synchronous computation with no durable coordination value.
Architecture¶
flowchart LR
parent[parent_workflow_orchestrator]
child1[customer_sync_sub_orchestrator]
child2[inventory_sync_sub_orchestrator]
activity1[sync_customer_profile activity]
activity2[build_inventory_snapshot activity]
result[aggregated parent result]
parent -->|call_sub_orchestrator| child1
parent -->|call_sub_orchestrator| child2
child1 -->|call_activity| activity1
child2 -->|call_activity| activity2
child1 --> parent
child2 --> parent
parent --> result
Behavior¶
sequenceDiagram
participant Starter as HTTP starter
participant Parent as Parent orchestrator
participant Customer as Customer sub-orchestrator
participant Inventory as Inventory sub-orchestrator
participant Activity as Activity functions
Starter->>Parent: start_new("parent_workflow_orchestrator")
Parent->>Customer: call_sub_orchestrator("customer_sync_sub_orchestrator")
Customer->>Activity: call_activity("sync_customer_profile")
Activity-->>Customer: customer sync result
Customer-->>Parent: child result
Parent->>Inventory: call_sub_orchestrator("inventory_sync_sub_orchestrator")
Inventory->>Activity: call_activity("build_inventory_snapshot")
Activity-->>Inventory: inventory snapshot result
Inventory-->>Parent: child result
Parent-->>Starter: final aggregated output via status endpoint
Prerequisites¶
- Python 3.10+
- Azure Functions Core Tools v4
- Durable storage configured in local settings
azure-functions,azure-functions-durable, andazure-functions-logging-pythoninstalled
Project Structure¶
examples/orchestration-and-workflows/sub_orchestration/
|- function_app.py
|- host.json
|- local.settings.json.example
|- pyproject.toml
`- README.md
Implementation¶
The starter launches the parent orchestration and returns the standard Durable status URLs.
@app.route(route="start-sub-orchestration", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
@app.durable_client_input(client_name="client")
async def start_sub_orchestration(req: func.HttpRequest, client: df.DurableOrchestrationClient):
instance_id = await client.start_new("parent_workflow_orchestrator", None, payload)
return client.create_check_status_response(req, instance_id)
The parent orchestrator calls two child orchestrators in sequence and aggregates their outputs.
@app.orchestration_trigger(context_name="context")
def parent_workflow_orchestrator(context: df.DurableOrchestrationContext):
payload = context.get_input() or DEFAULT_INPUT
customer_result = yield context.call_sub_orchestrator("customer_sync_sub_orchestrator", payload)
inventory_result = yield context.call_sub_orchestrator("inventory_sync_sub_orchestrator", payload)
return {
"customer": customer_result,
"inventory": inventory_result,
}
Each child orchestrator remains small and only schedules its own activity. That keeps orchestration boundaries explicit while still allowing the parent to compose them.
@app.orchestration_trigger(context_name="context")
def customer_sync_sub_orchestrator(context: df.DurableOrchestrationContext):
payload = context.get_input() or DEFAULT_INPUT
return (yield context.call_activity("sync_customer_profile", payload))
Logging belongs in the starter and activities, not inside orchestrator replay paths.
This example uses azure-functions-logging-python for structured application logs while the durable
runtime manages orchestration history separately.
Run Locally¶
Expected Output¶
POST /api/start-sub-orchestration -> 202 Accepted
Final orchestration output:
{
"instanceId": "<parent-instance-id>",
"customer": {
"step": "customer_sync",
"customerId": "cust-1001",
"segment": "enterprise",
"status": "completed"
},
"inventory": {
"step": "inventory_sync",
"skuCount": 2,
"status": "completed"
}
}
Production Considerations¶
- Composition: use sub-orchestrations to isolate reusable workflow segments and failure domains.
- Retries: apply retry policies at the child orchestration or activity level for transient faults.
- Idempotency: keep activities idempotent because child workflows can replay or retry independently.
- Observability: log parent and child correlation identifiers in starter and activity code.
- Versioning: evolve child orchestrators carefully because in-flight durable instances persist history.