Azure Functions or Foundry Agent Service Is the Wrong Question

Enterprise teams keep asking me the same architecture question. Should agent work run in Azure Functions or in Foundry Agent Service? The question sounds like a choice between two products. It is not. It is a question about where you draw the Foundry Agent Service architecture boundary, and the honest answer changed in the last twelve months while most of the diagrams did not.

The Foundry Agent Service architecture boundary, as commonly drawn

Here is the version of the pattern I see repeated in decks, posts, and internal architecture docs.

Azure Functions sits below the line as the action layer. Tools, action handlers, data retrieval, API adapters. Stateless, discrete, testable, independently scalable. Foundry Agent Service sits above the line as the orchestration and reasoning layer. Thread state, tool coordination, evaluation, governance. The banner at the bottom says to keep orchestration in Foundry and actions in Functions.

Most of that diagram is still correct. One box in it is now wrong, and it happens to be the box the whole argument rests on.

The claim that stopped being true

The reasoning behind the pattern went like this. Azure Functions is a stateless runtime. If you build agent orchestration inside it, you end up hand building a state machine: persisting conversation turns yourself, tracking which tool calls already completed, writing your own resume logic after a crash, and owning every retry path end to end. That is real work, it is easy to get wrong, and Foundry Agent Service does it for you. So orchestration belongs above the line.

In 2025 that argument held. Then the Durable Task extension for Microsoft Agent Framework shipped, and Azure Functions acquired the thing it was said to lack.

Durable agents give you persistent sessions that survive process crashes, restarts, and scaling events. They checkpoint automatically, so completed steps do not re-execute on recovery. Sequential, fan out and fan in, and conditional routing across multiple agents all work, with context.GetAgent() pulling a registered agent into an orchestration. And the whole thing runs on the Flex Consumption plan and scales to zero.

That is a managed state machine, in Azure Functions, supported by Microsoft. The strongest paragraph in the received pattern now argues against a product that exists.

Preview status, read from the feed rather than the docs

Note the status carefully, because this is where enthusiasm outruns the facts. The core Microsoft Agent Framework packages are stable. The durable hosting packages are not. Microsoft.Agents.AI.Hosting.AzureFunctions and Microsoft.Agents.AI.DurableTask have never had a stable release. Neither Learn page carries a preview banner, but every documented install command passes --prerelease, and the feed agrees.

Building the sample turned up something sharper than the package labels. Microsoft.Agents.AI.Foundry 1.19.0 binds Azure.AI.Projects 2.1.0.0, and version 2.1.0 exists only as a beta. Pin the stable 2.0.1 underneath it and the compiler refuses with a version conflict. So the maturity question does not stop at the hosting packages. Today, the current Agent Framework Foundry integration cannot be assembled on top of a stable data plane SDK at all.

Say that out loud in an architecture review, and cite the package feed rather than the docs.

Three substrates above the Foundry Agent Service architecture boundary

Once you accept that Functions can orchestrate, the Foundry Agent Service architecture boundary stops being a layer cake and becomes a decision.

Foundry prompt agents. Configuration rather than runtime code. Foundry owns the thread state, the lifecycle, the tracing, the evaluation, and the retry behavior. This is the fastest route to a governed production agent, and it costs you the least code and the least control. When a team wants an agent in front of existing APIs and does not want to run anything, this is the answer.

Durable agents on Azure Functions. Your orchestration, in C#, with checkpointing supplied by the durable runtime. You own the deployment pipeline, the infrastructure as code, the tests, the alerting, and the pager. You get deterministic, inspectable control flow and the same operational tooling as the rest of your estate. When the orchestration has real branching, compensation, or long running steps that must survive a restart, this earns its keep.

Foundry hosted agents. Your Agent Framework or LangGraph code, shipped as a container image or a zip into Foundry’s runtime. Foundry supplies the endpoint, the scaling, a dedicated Microsoft Entra identity per agent, and session level state. You supply the code. This is the case that blurs the boundary the original diagram insists on keeping clean, because neither side owns the whole thing.

One action layer below the line

Below the line, nothing changes. Azure Functions remains the action and tool layer for all three. You surface tools over MCP at the Functions webhook endpoint, register them once in a Foundry toolbox (I covered how that moves tool authentication off the agent in my Foundry Toolboxes post), and let any substrate above discover them at runtime. The tool code does not know or care which orchestrator called it, and hardening it is ordinary API work of the kind I described in Azure Functions behind API Management.

The companion repo, and what it proves

I built a companion repo to check that this holds in practice rather than only in a diagram. It contains one action layer and three orchestrators over it, running the same order fulfillment task. You can find it at function-foundry-boundry.

All three substrates ran against the deployed action layer. The durable agent is deployed and measured, with the smoke suite passing twelve of twelve: the action layer, the idempotency guarantee, and a full orchestration ending in exactly one customer notification. Its prompt agent sibling was created against the live MCP endpoint and driven from the Foundry playground. And the hosted agent is registered and active in Foundry, with its container exercised end to end over the responses protocol self hosted; the one path I have not driven is invoking that container through Foundry’s own runtime. Getting all of this working took forty one documented findings, every one listed in the repo’s verification log, and several of them taught me more than the sample did. The repo states its coverage in the same words, because a sample that overstates what it proved is worse than no sample.

What actually differs, in code

Building all three surfaced a difference I did not expect to be so concrete.

The action layer exposes five operations: look up an order, check inventory, reserve stock, arrange delivery, and notify the customer. Two of those change state a customer can see. Reserving stock commits inventory. Notifying the customer sends a message that cannot be unsent.

In the durable agent, those two operations do not appear in the model’s tool set at all. The orchestrator calls them as activities, where the durable runtime commits them exactly once even if the surrounding agent turns replay. What the model gets is three read oriented tools and the job of producing a plan, while the runtime executes the parts that spend money.

In the prompt agent and the hosted agent, there is no activity boundary to put them behind. The model itself has to be trusted to call each one exactly once, and your protection is instructions plus an idempotency key enforced by the tool.

That asymmetry is the trade, stated in about twelve lines. It is not a bug in either approach. It is what “who owns retry and failure handling” means once you stop discussing it abstractly.

Three substrates, three failures, one shape

I did not have to wait long for the trade to show itself in data. All three substrates ran against the same action layer on the same day, with instructions that said, in each case, to notify the customer exactly once and to use the delivery slot they were given. The prompt agent fulfilled its order correctly and notified the customer twice, in two differently worded messages, because two generations each decided a message was warranted; the reservation, protected by the idempotency key rather than by instructions, did not duplicate. The hosted agent notified exactly once, and booked the delivery for the literal phrase “next available”, because with no orchestrator in the stack nobody owns the clock, so the model passed prose where a date belonged and the action layer recorded it faithfully. And the durable agent, in an early revision that let the model judge whether stock sufficed, refused a fulfillable order with twelve units on the shelf, which is why the shipped version keeps the model out of that decision entirely. Three substrates, three different failures, one shape: whatever the runtime owned, held. Whatever the model was merely instructed about, eventually did not.

One practical note for anyone following the same path. The durable types are split across three namespaces that do not follow from one another: AIAgent, AgentSession and AgentResponse<T> come from Microsoft.Agents.AI, DurableAIAgent and context.GetAgent from Microsoft.Agents.AI.DurableTask, and ConfigureDurableAgents from Microsoft.Agents.AI.Hosting.AzureFunctions. The DurableTask package is documented as the bring your own compute option, which makes it easy to leave out of a Functions project. You need it on both paths, because the hosting package does not surface the orchestration types.

The one control that survives the decision

Which brings up the idempotency key. Every substrate can retry a tool call, and every substrate can replay. So the action layer refuses to care which one is calling and enforces safety itself. reserve_stock takes a reservation key and returns the existing reservation rather than holding stock twice. The key derives from the order, never from the run. A run derived key is indistinguishable from no key at all, because every retry invents a fresh one. I measured that exact failure in an earlier sample, and it produced two authorizations against a balance that only had room for one.

Put the idempotency key in the action layer regardless of which substrate you pick. It is the one control that survives the architecture decision.

What the durable orchestration looks like

The reason this is worth showing rather than describing is that the code contains almost no plumbing. Here is the middle of the fulfillment orchestration, trimmed for length.

[Function(nameof(FulfilOrder))]
public static async Task<FulfilmentOutcome> FulfilOrder(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var request = context.GetInput<FulfilOrderRequest>();
DurableAIAgent agent = context.GetAgent(AgentName);
AgentSession session = await agent.CreateSessionAsync();
// Facts come from activities, never from the model.
var order = await context.CallActivityAsync<Order?>(
nameof(ToolActivities.LookupOrder), request.OrderId);
if (order is null) { /* unknown order, return */ }
// The key derives from the order, never from the run. And there is no stock
// pre-check: reserve_stock IS the stock check, enforced atomically by the
// action layer, which is the only answer that cannot go stale between
// checking and reserving.
var reservationKey = $"resv-{request.OrderId}";
var reservation = await context.CallActivityAsync<ReservationResult>(
nameof(ToolActivities.ReserveStock),
new ReserveArgs(order.Sku, order.Quantity, reservationKey));
if (reservation.Reservation is null) { /* notify once with the action layer's reason, return */ }
// Only now does the model get a turn: book the slot the orchestrator computed,
// and write the customer message. Checkpointed: on replay this returns from
// history without calling the model.
AgentResponse<FulfilmentPlan> plan = await agent.RunAsync<FulfilmentPlan>(
message: $"Stock is held for {request.OrderId}. Call arrange_delivery with " +
$"the slot exactly as '{slot}'. Then write one short message " +
"telling the customer what happens next.",
session: session);
// ...
}

Count what is absent. There is no thread table, no bookkeeping to work out which steps already ran, no resume logic, and no retry loop. context.GetAgent pulls in an agent registered at startup, each RunAsync is a checkpoint, and CallActivityAsync marks the operations that must commit exactly once.

When the model judged a fact

An earlier revision opened this orchestration with a triage turn: the agent was told to look the order up, check inventory, and decide whether there was enough free stock. It worked all day, and then, with twelve units free and two wanted, it returned canFulfil false with the reason “Free stock is lower than the order quantity”. The tools had been called, and the inventory tool had answered twelve. Yet the model fumbled a comparison a CPU does perfectly, the orchestrator took its word for it, and a customer was told an order could not be fulfilled while eleven spare units sat on the shelf. The division of labour above is the correction: activities fetch facts, the action layer enforces constraints, and the model plans and writes, which is the work only a model can do. It never decides whether.

Registration is equally short. In Program.cs you build an ordinary Agent Framework agent against the Foundry project, then hand it to the durable host.

AIAgent agent = new AIProjectClient(new Uri(foundryEndpoint), new DefaultAzureCredential())
.AsAIAgent(model: modelDeployment, instructions: Instructions, name: AgentName,
tools: [ /* the read oriented tools only */ ]);
builder.ConfigureDurableAgents(options => options.AddAIAgent(agent));

Compare that against what the same behavior cost in 2025, and you can see why the received pattern needs updating rather than repeating.

What deploying it actually cost

I would not have found any of the following by reading documentation, and three of them would sink a production build.

The preview and stable halves of one product are on different trains. Microsoft.Agents.AI is stable at 1.19. Microsoft.Agents.AI.DurableTask and Microsoft.Agents.AI.Hosting.AzureFunctions are preview at 1.16. The durable packages hook into internals of the core package, nothing declares an upper bound, and NuGet pairs them without a word. It restores, compiles, deploys and starts. The agent answers in eighteen seconds. Then the orchestrator fails a determinism check, gets retried forever, and reports Running until you go looking. No compiler error, no restore warning, no startup error.

The fix is to pin the whole family to the older train, which means pinning the stable package down to match the previews. That is worth saying out loud in a review, because it is backwards from how anyone manages versions.

Failures that look like working systems

Two Azure Functions apps must not share a deployment container. Flex Consumption keeps the deployed package in the container you name. Point two apps at the same one and the second deployment overwrites the first, so an app runs the other app’s code. It does not fail. The host starts cleanly, registers whatever it found, and every route you wrote returns 404. The only reliable tell is comparing the registered function list against what you wrote.

A client that turns failures into nulls defeats the whole point of a durable activity. My first tool client returned null on any non-success status. When a function key went missing, the agent received “no such order” and reasoned confidently about a fabricated premise, while the notification activity returned success having done nothing. Exactly-once is only a guarantee if a failed call is allowed to look like one. An activity that swallows a transport error is worse than one that crashes, because neither the runtime’s retry nor the orchestration’s error handling ever engages.

Two smaller ones worth carrying. In-process state in the action layer is simply wrong on a platform that scales out, and moving reservations to Table Storage made the idempotency key stronger than the lock it replaced, because a duplicate row key returns 409 atomically across every instance. And the delivery date belongs in the orchestrator, not the prompt: an orchestrator must be deterministic, so time comes from the replay-safe clock. Ask a model for “the earliest weekday morning slot” and you will get a booking reference containing the literal phrase, which is funny once and then a determinism bug forever.

Where the platform enforces the Foundry Agent Service architecture boundary

One thing I expected to argue for turned out not to need arguing. Flex Consumption allows exactly one site per plan, so the action layer and the orchestrator cannot share one. The received diagram calls the action layer “independently scalable” as a design aspiration you could honour or skip. On this platform you have no choice: the two sides of the boundary scale on separate plans by construction and cannot starve each other of instances.

That is a better argument for drawing the line than any of mine, because it is a constraint rather than an opinion.

The four questions that settle it

When teams argue about this, they argue about product features. The argument ends faster if you ask four operational questions instead.

Who owns retry and failure handling when a tool call dies? With a prompt agent, Foundry does. With a durable agent, you do, and you decide the policy. And finally, with a hosted agent, it splits, and the split is the thing to write down.

Where does session state live, and in whose subscription? All three substrates support bring your own resources for conversation state, using Azure Storage, Cosmos DB, or Azure AI Search inside your own subscription and under your own retention policy. This matters enormously for data residency, and it is also why residency alone cannot pick a substrate for you. It used to be the deciding argument. It is now available on every path.

Which control plane does the on call engineer open at three in the morning? This question ends more debates than the other three combined. If the answer is “it depends which agent”, you have not drawn a boundary, you have accumulated one.

Is the topology documented, or improvised per team? Teams that struggle operationally are rarely the ones that picked the wrong substrate. They are the ones that picked different substrates for different agents without recording why, and inherited the failure modes of both with the runbook of neither.

Where the Foundry Agent Service architecture boundary is the wrong frame

I have made this sound tidier than it is. Several situations break the framing above.

When the orchestration is genuinely trivial. A single agent, three tools, no branching, no long running steps. Reaching for a durable orchestrator here buys you preview packages, a Durable Task backend, and a deployment pipeline in exchange for checkpointing you will never exercise. Use a prompt agent and move on.

When your team has no .NET or Python depth. The durable path is code. It rewards a team that already runs Functions, already has infrastructure as code, and already reviews pull requests. It punishes a team that wanted an agent and got a distributed systems project.

Constraints that make the decision for you

When the preview status is disqualifying. Some regulated programs cannot take a dependency on packages with no stable release, whatever the docs imply. That constraint is legitimate and it makes the decision for you. Prompt agents and hosted agents are generally available. Say so in the review and let the constraint do its work.

Do not soften this one. The train skew above is exactly the class of failure that preview status is warning you about, and it cost most of a day to find in a sample with four files of orchestration.

When you actually need multi agent collaboration across organizational boundaries. The Agent2Agent protocol appears in a lot of diagrams, including the one at the top of this post, positioned as a settled capability alongside thread state and tool coordination. It is still preview. Design for it if you like, but do not commit a program to it this quarter.

When you are migrating rather than starting. If you are on Agents classic, the deprecation deadline is doing your planning for you. Classic retires on 31 March 2027, and the migration guide makes clear it is not a rename. That is not far away for an enterprise program with a change freeze and a release train.

Draw the Foundry Agent Service architecture boundary before anyone writes code

The original pattern got the important thing right. There is a boundary, it matters, and architectures fail when nobody draws it. What it got wrong was assuming the Foundry Agent Service architecture boundary always falls in the same place.

It does not. You now have three legitimate substrates above the line and one action layer below it. The action layer stays where it is, and the tools you write for it survive every one of these decisions, which is a good reason to build that layer first and pick the substrate second.

Pick one orchestration substrate per workload. Write down which one and why. Then let the four questions above, rather than a diagram, decide the next one.

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