Durable Functions as the Orchestration Layer for Directed Agentic Workflows

Not all AI orchestration should be autonomous. Some scenarios need predictable, directed steps, and that is where Durable Functions fit in the Azure Functions AI stack.

The previous post in this series covered the serverless agents runtime, where you give the agent instructions and tools and Microsoft Agent Framework determines the execution path. This post covers the opposite end of the spectrum: Durable Functions agentic workflows, where you define the steps and the model executes them in a known sequence. The workflow is deterministic. The AI is a participant, not the orchestrator.

If your AI-driven process has fixed, ordered steps and you need auditability, fault tolerance, and long-running execution, Durable Functions is the right tool. If you want a model to determine the steps dynamically, go back to the serverless agents runtime.

What Durable Functions brings to agentic scenarios

Durable Functions is an extension of Azure Functions that lets you build stateful workflows in a serverless environment. The runtime manages state, checkpoints, retries, and recovery so workflows can run reliably for long periods minutes, hours, or days.

The doc positions it clearly for agentic use: “Some scenarios require a higher level of predictability or well-defined steps. These directed agentic workflows orchestrate separate tasks or interactions that agents must follow.”

Three capabilities make Durable Functions particularly well-suited for AI orchestration:

  • State persistence across steps. A workflow that calls an LLM, waits for a human decision, calls another service, and then writes a result can span hours or days without holding a connection open or burning compute. Durable Functions checkpoints state after every activity function completes.
  • Built-in retry and fault tolerance. LLM calls fail. External services time out. Durable Functions handles retries at the activity level with configurable backoff. You define the retry policy once, and every step in the workflow inherits it.
  • Human-in-the-loop support. The external events pattern lets a workflow pause and wait for human input — an approval, a correction, a classification decision. The workflow resumes when the event arrives, with full state intact.

The four patterns that map to agentic AI

Durable Functions has several application patterns documented in the Microsoft Learn docs. Four map directly to common agentic AI scenarios.

The patterns below are illustrated in Python, which maps clearly to the Durable Functions programming model. The companion repository implements all five patterns in C# (.NET 8, isolated worker). The orchestrator and activity structure are identical, but the syntax differs.

Function chaining — sequential AI pipeline

The simplest pattern: step A completes, then step B runs, then step C. Each step takes the previous step’s output as input.

In agentic terms, this is the document processing pipeline: extract text → classify intent → call LLM with classification context → write structured result. Each activity function is independently retryable. If the LLM call fails, Durable Functions retries it without re-running extraction.

@app.orchestration_trigger(context_name="context")
def document_pipeline_orchestrator(context: df.DurableOrchestrationContext):
text = yield context.call_activity("extract_text", context.get_input())
classification = yield context.call_activity("classify_intent", text)
result = yield context.call_activity("call_llm", {
"text": text,
"classification": classification
})
yield context.call_activity("write_result", result)
return result

The orchestrator function contains no business logic; it coordinates. The activity functions contain the work. This separation makes each step independently testable and observable.

Fan-out/fan-in — parallel retrieval and aggregation

The orchestrator starts multiple activity functions simultaneously and waits for all to complete before continuing. This is the RAG retrieval pattern: query multiple data sources in parallel, collect all results, and pass the aggregated context to the LLM.

@app.orchestration_trigger(context_name="context")
def parallel_retrieval_orchestrator(context: df.DurableOrchestrationContext):
query = context.get_input()
# Fan out — all three retrieval calls run in parallel
tasks = [
context.call_activity("search_knowledge_base", query),
context.call_activity("search_policy_documents", query),
context.call_activity("search_case_history", query),
]
results = yield context.task_all(tasks)
# Fan in — aggregate and call LLM once with full context
response = yield context.call_activity("call_llm_with_context", {
"query": query,
"context": results
})
return response

The Durable Functions tutorial in the Learn docs demonstrates this pattern with parallel text file analysis: multiple files are processed simultaneously, results are aggregated, and a single output is returned.

Human interaction — approval and correction loops

The external events pattern lets a workflow pause indefinitely waiting for human input. This is the compliance review pattern: the AI produces a draft or classification, a human reviews it, and the workflow continues with the human’s decision.

@app.orchestration_trigger(context_name="context")
def approval_orchestrator(context: df.DurableOrchestrationContext):
input_data = context.get_input()
# AI produces initial classification
ai_result = yield context.call_activity("classify_with_ai", input_data)
# Notify reviewer and wait — the workflow pauses here
yield context.call_activity("notify_reviewer", {
"result": ai_result,
"instance_id": context.instance_id
})
# Wait for human decision — could be minutes or days
human_decision = yield context.wait_for_external_event("ReviewDecision")
# Continue with human-approved or corrected result
final_result = yield context.call_activity("process_decision", {
"ai_result": ai_result,
"human_decision": human_decision
})
return final_result

The workflow pauses at wait_for_external_event without consuming resources. When the reviewer submits their decision, the Durable Functions client sends the event and the workflow resumes immediately.

Monitor — polling until a condition is met

The monitor pattern runs a check on a schedule until a condition is satisfied, then exits or escalates. In agentic terms: poll an external system until a document is processed, a model inference job completes, or a status changes.

@app.orchestration_trigger(context_name="context")
def monitor_orchestrator(context: df.DurableOrchestrationContext):
input_data = context.get_input()
expiry = context.current_utc_datetime + timedelta(hours=24)
while context.current_utc_datetime < expiry:
status = yield context.call_activity("check_processing_status", input_data)
if status == "completed":
return yield context.call_activity("retrieve_result", input_data)
elif status == "failed":
return yield context.call_activity("handle_failure", input_data)
# Wait before next poll — no compute consumed during wait
next_check = context.current_utc_datetime + timedelta(minutes=5)
yield context.create_timer(next_check)
return yield context.call_activity("handle_timeout", input_data)

A real pattern: risk-class-driven routing

A pattern I work with in integration architecture is risk-class-driven routing — an AI classification step followed by different downstream workflows depending on the risk class assigned.

The AI classifies an incoming request as low, medium, or high risk. The orchestrator branches based on the classification:

  • Low risk — automated processing, result written directly
  • Medium risk — automated processing with human notification and override window
  • High risk — human review required before any processing continues
@app.orchestration_trigger(context_name="context")
def risk_routing_orchestrator(context: df.DurableOrchestrationContext):
request = context.get_input()
# AI classification step
risk_class = yield context.call_activity("classify_risk", request)
if risk_class == "LOW":
return yield context.call_activity("process_automated", request)
elif risk_class == "MEDIUM":
result = yield context.call_activity("process_automated", request)
yield context.call_activity("notify_supervisor", {
"result": result,
"override_window_minutes": 30
})
try:
override = yield context.wait_for_external_event(
"SupervisorOverride",
timeout=timedelta(minutes=30)
)
return override
except TimeoutError:
return result
else: # HIGH
yield context.call_activity("notify_reviewer", request)
human_decision = yield context.wait_for_external_event("ReviewDecision")
return yield context.call_activity("process_with_decision", {
"request": request,
"decision": human_decision
})

This pattern appears in healthcare authorization, financial transaction review, and any domain where the cost of an incorrect automated decision varies by risk level. Durable Functions is the right runtime because it handles the human-in-the-loop wait without consuming compute, and it checkpoints state so a mid-workflow restartdoesn’t lose the AI classification result.

The companion repository implements all five patterns in C# with full azd deployment. The TESTING.md file contains the exact curl commands to exercise each pattern, including submitting external events for the approval and risk-routing workflows, all verified on Azure.

When Durable Functions is not the right answer

The directed vs autonomous distinction is the primary decision. But two other constraints matter.

  • Avoid it for very short workflows. If your workflow completes in under a second and has no human-in-the-loop steps, the Durable Functions overhead storage writes and checkpoint reads adds latency you don’t need. A simple function chain without orchestration is faster and cheaper.
  • Avoid it when the steps are not known in advance. If the AI needs to decide dynamically which tools to call and in what order, Durable Functions cannot model that. That is the serverless agents runtime; the AI is the orchestrator, not a participant.
  • Consider Logic Apps Agent Loop for low-code scenarios. If the workflow involves mostly connector-based integrations rather than custom code, Logic Apps with its Agent Loop pattern may be the right choice. Durable Functions earns its place when you need custom code logic at each step, tight control over retry behavior, or the ability to unit test each activity function independently.

The storage backend — Durable Task Scheduler

One operational detail worth knowing before you deploy: Durable Functions needs a storage backend to persist workflow state. The recommended option is Durable Task Scheduler, a managed service that handles task hub storage without requiring you to manage Azure Storage queues and tables manually.

For agentic workflows, which may run for hours and involve many checkpoints, the Durable Task Scheduler is worth the setup once it is fully available. At the time of writing, the azureManaged storage provider requires a preview extension bundle that isn’t included in the current release. The default Azure Storage backend works correctly for all patterns and is what the companion repo uses. Check the Durable Task Scheduler quickstart for the current availability status before planning a production deployment.

Dynamic workflows — the bridge between the two runtimes

The spectrum diagram at the top of this post places Durable Functions on the directed end and the serverless agents runtime on the autonomous end. Dynamic workflows, an experimental feature in the serverless agents runtime, sits between the two, and it is worth knowing about before you commit to one pattern.

The concept: flip workflows.enabled: true in a .agent.md file’s front matter, and the agent gains five built-in tools, including start_workflow. When the agent decides the work is workflow-shaped, a multi-step plan, a fan-out across data sources, a wait; it calls start_workflow with a DAG of tasks. The runtime validates the DAG and launches it as a Durable Functions orchestration. The agent gets back a workflow_id immediately and ends its turn. The Durable orchestration runs the plan in the background.

The AI authors the plan. Durable Functions guarantees the execution

This is a meaningful architectural shift. With the patterns in this post, you write the orchestrator code; you define the steps. With dynamic workflows, the LLM authors the DAG at runtime based on the task it is given. The execution is still deterministic and fault-tolerant because Durable Functions is running it, but the plan itself is emergent. Three concrete advantages the docs cite over chaining tool calls in conversation:

  • Lower token cost. Intermediate task results stay inside the orchestration. The agent sees only the final completion envelope, not every fan-out result. The docs reference roughly a 10× reduction on multi-tool workflows.
  • Lower latency. Each direct tool call is a model round-trip. A 20-step plan is one model turn to author the workflow, not 20.
  • Context-window discipline. Hundreds of kilobytes of intermediate data log lines, search hits, and line items never reach the model’s context. The agent reasons over the summary.

How it works in practice. Workflow tools are Python functions decorated with @workflow_tool rather than the standard @tool. The agent authors a DAG of tool tasks and wait tasks with depends_on edges for sequencing. ${node_id.result} templates let upstream outputs flow into downstream task arguments; the resolution happens inside the orchestrator, not in the agent’s context.

{
"tasks": [
{ "id": "fetch_a", "type": "tool", "tool": "fetch_logs", "args": {"service": "auth"} },
{ "id": "fetch_b", "type": "tool", "tool": "fetch_logs", "args": {"service": "api"} },
{ "id": "summarize", "type": "tool", "tool": "summarize",
"args": {"sources": ["${fetch_a.result}", "${fetch_b.result}"]},
"depends_on": ["fetch_a", "fetch_b"] }
]
}

The incident triage sample in the azure-functions-agents-runtime repo shows this working end to end: an agent that fans out log fetches across multiple services, waits, then summarises the evidence.

What to know before using it. Dynamic workflows are experimental v1 with real constraints:

  • workflows.enabled: true is currently only honored on main.agent.md — dedicated agents can’t use it yet. That is flagged as a v2 constraint to lift.
  • v1 handlers must be synchronous. No per-task retry or timeout policies yet — those are v2.
  • The plan cap is 50 nodes, 10 parallel tasks, and a 24-hour maximum wait duration.
  • Completion is poll-based. The chat UI polls GET /agents/{slug}/workflows and injects a synthetic user message when a workflow reaches a terminal state, which triggers the agent to call get_workflow_status and summarise.

What comes next

The next post covers the fourth AI pattern: building RAG pipelines with Azure Functions, event-driven data retrieval at scale, the Azure OpenAI binding extension, and where APIM fits as the LLM gateway layer.

Up next: Building RAG Pipelines with Azure Functions: Event-Driven Data Retrieval at Scale