Usage¶
This guide covers production patterns for azure-functions-durable-graph in the Azure
Functions Python v2 programming model with Durable Functions.
If you are new to the package, start with Quickstart and then return here for deeper patterns.
Baseline Pattern¶
from pydantic import BaseModel
from azure_functions_durable_graph import DurableGraphApp, ManifestBuilder
class OrderState(BaseModel):
order_id: str
validated: bool = False
shipped: bool = False
def validate_order(state: OrderState) -> dict:
return {"validated": True}
def ship_order(state: OrderState) -> dict:
return {"shipped": True}
builder = ManifestBuilder(graph_name="order_flow", state_model=OrderState)
builder.set_entrypoint("validate")
builder.add_node("validate", validate_order, next_node="ship")
builder.add_node("ship", ship_order, terminal=True)
runtime = DurableGraphApp()
runtime.register_registration(builder.build())
app = runtime.function_app
Mental model
Think of the manifest as an intermediate representation: graph topology is compiled once at startup, and the orchestrator reads it without executing arbitrary user code.
Sequential flow¶
The simplest pattern chains nodes with next_node:
builder.add_node("a", handler_a, next_node="b")
builder.add_node("b", handler_b, next_node="c")
builder.add_node("c", handler_c, terminal=True)
Conditional routing¶
Use a route handler to pick the next node dynamically:
from azure_functions_durable_graph import RouteDecision
def route_after_classify(state: TicketState) -> RouteDecision:
if state.category == "urgent":
return RouteDecision.next("escalate")
return RouteDecision.next("auto_reply")
builder.add_node("classify", classify, route=route_after_classify)
builder.add_node("escalate", escalate, terminal=True)
builder.add_node("auto_reply", auto_reply, terminal=True)
String shorthand¶
Route handlers can return a plain string instead of RouteDecision:
def route_after_classify(state: TicketState) -> str:
if state.category == "urgent":
return "escalate"
return "auto_reply"
Use "__complete__" to signal graph completion from a route handler.
External events (human-in-the-loop)¶
For approval workflows or human-in-the-loop patterns, use wait_for_event:
def route_after_review(state: ReviewState) -> RouteDecision:
if state.needs_approval:
return RouteDecision.wait_for_event(
event_name="manager_approval",
resume_node="apply_decision",
)
return RouteDecision.next("apply_decision")
def merge_approval(state: ReviewState, payload: dict) -> dict:
return {
"approved": payload.get("approved", False),
"reviewer": payload.get("reviewer"),
}
builder.add_event_handler("manager_approval", merge_approval)
Sending events via HTTP¶
curl -X POST http://localhost:7071/api/runs/{instance_id}/events/manager_approval \
-H "Content-Type: application/json" \
-d '{"approved": true, "reviewer": "alice@example.com"}'
State merging¶
Node handlers return partial state updates. The runtime merges them:
def handler(state: MyState) -> dict:
# Only updates "processed" — other fields are preserved
return {"processed": True}
Merge rules:
dictreturn → shallow merge into current stateBaseModelreturn → full replacement (validated against state model)Nonereturn → state unchanged
Shallow merge
Dict returns perform shallow merge. Nested dicts are replaced entirely, not recursively merged.
Async handlers¶
Both sync and async handlers are supported:
async def call_llm(state: AgentState) -> dict:
response = await llm_client.complete(state.prompt)
return {"llm_response": response}
The runtime uses _maybe_await internally to handle both transparently.
Multiple graphs¶
Register multiple graphs on a single DurableGraphApp:
runtime = DurableGraphApp()
runtime.register_registration(order_flow_registration)
runtime.register_registration(support_agent_registration)
app = runtime.function_app
Each graph gets its own API route: /api/graphs/{graph_name}/runs.
Cancelling runs¶
curl -X POST http://localhost:7071/api/runs/{instance_id}/cancel \
-H "Content-Type: application/json" \
-d '{"reason": "user requested cancellation"}'
Graph versioning¶
The manifest includes a graph_hash derived from the canonical JSON of the full
manifest (graph name, version, state model, nodes, edges, event handlers, and metadata).
This hash changes when any part of the manifest changes, enabling safe
side-by-side deployments.
Testing graph registrations¶
Test your graph logic without Azure Functions infrastructure:
import pytest
from pydantic import BaseModel
from azure_functions_durable_graph import ManifestBuilder
class State(BaseModel):
value: int = 0
def increment(state: State) -> dict:
return {"value": state.value + 1}
def test_graph_build():
b = ManifestBuilder(graph_name="test", state_model=State)
b.set_entrypoint("inc")
b.add_node("inc", increment, terminal=True)
reg = b.build()
assert reg.manifest.graph_name == "test"
assert reg.manifest.entrypoint == "inc"
assert "inc" in reg.manifest.nodes
Common gotchas¶
- Orchestrator determinism: never put LLM calls or I/O in the orchestrator — all user logic runs in activities.
- Terminal nodes cannot define
next_nodeorroute. - State model must be a Pydantic v2
BaseModel. DurableGraphAppregisters all HTTP routes and activities on construction.
See Troubleshooting for issue-by-issue fixes.