The Azure Functions Serverless Agents Runtime Explained

Let’s discuss the Azure Functions serverless agents runtime. Most conversations about agent runtimes on Azure land on Azure AI Foundry Agent Service. That is the right starting point for teams that want a fully managed, enterprise-grade agent host. But it is not the only option, and for event-driven scenarios, it is often not the best one.

The Azure Functions serverless agents runtime is a programming model that lets you define agents as function apps. Events, schedules, messages, or HTTP requests trigger agents. They run on Flex Consumption with scale-to-zero, managed identity, and Application Insights. And they are deployed with azd like any other function app.

This post explains what the runtime actually is, how its three configuration files work together, and where it fits relative to Foundry Agent Service and Durable Functions. If you are new to Azure Functions as an AI platform, start with Azure Functions AI Integration: The Quiet Powerhouse, which maps all four AI-enabled patterns. If you are looking specifically at MCP server hosting, Hosting MCP Servers on Azure Functions covers the three hosting options in detail.

How the Azure Functions serverless agents runtime works

The runtime is a programming model built on top of Azure Functions. When an event fires a timer, an HTTP request, or a queue message, the runtime starts the agent, runs it through Microsoft Agent Framework, and handles the trigger registration and endpoint wiring automatically.

You do not write trigger code or implement an agent loop. You define three files, deploy a function app, and the runtime does the rest.

Those three files are:

  • .agent.md โ€” defines the agent: its instructions, its trigger, and the tools it can use
  • agents.config.yaml โ€” app-wide runtime defaults, including the model deployment and any shared infrastructure (such as an Azure Container Apps dynamic session pool for sandboxed code execution)
  • mcp.json โ€” lists the remote MCP servers available to the agents in the app

The runtime discovers these files at startup, registers the required triggers and endpoints, and wires the agent to Microsoft Agent Framework. You can have multiple agents in a single function app, each defined in its own .agent.md file, all sharing the app-wide configuration.

What the Azure Functions serverless agents runtime deploys

The Microsoft Learn quickstart deploys two agents from a single function app:

Chat agent (main.agent.md) โ€” an HTTP-triggered agent that exposes a debug chat UI in the browser. It can execute sandboxed Python code via an Azure Container Apps dynamic session pool and browse the web. No email tooling.

Blog summary agent (daily_microsoft_blog_summary.agent.md) โ€” a timer-triggered agent. The YAML front matter in the file declares the schedule; the markdown body contains the agent instructions. On each timer fire, the agent gathers recent Microsoft blog posts, summarises them, and emails the digest via a managed MCP server connected to Microsoft 365 Outlook.

What gets provisioned by azd up for this template:

ResourcePurpose
Flex Consumption function appHosts the agents
Azure AI Foundry project + model deploymentLLM for agent reasoning
Azure Container Apps dynamic session poolSandboxed Python code execution
Storage accountFunction app state
Application InsightsMonitoring
Connector Namespace + M365 Outlook connectionEmail delivery (optional)
Managed MCP serverExposes the Outlook connector to agents

The provisioning is handled entirely by Bicep via azd โ€” you do not configure any of this manually.

How the agent definition files work

.agent.md is a markdown file with YAML front matter. The front matter declares the trigger and any agent-level configuration. The markdown body is the system prompt โ€” the instructions the agent follows when it runs.

The timer-triggered blog summary agent front matter looks roughly like:

---
trigger:
type: timer
schedule: "0 0 8 * * *"
tools:
- mcp_server: outlook
---

The markdown body below contains the agent’s instruction set; it tells the agent what to gather, how to summarise it, and how to format the email. You write it in plain English.

agents.config.yaml sets defaults that apply across all agents in the app. The model deployment lives here, so every agent uses the same Azure AI Foundry model unless you override it. This setting also defines the session pool endpoint for sandboxed code execution.

mcp.json lists the remote MCP servers the agents can call. The quickstart template includes a managed MCP server for the Microsoft 365 Outlook connector when you enable email delivery. The runtime reads this file at startup and makes those servers available to all agents in the app.

How Azure Functions Serverless Agents Runtime differs from Foundry Agent Service

The distinction matters for architecture decisions.

Foundry Agent Service is a fully managed service. Microsoft operates the agent host. You configure agents through the Foundry portal or SDK, connect tools, and the service handles orchestration, state, and scaling. It has enterprise SLAs, built-in tooling, and a managed lifecycle.

The serverless agents runtime is a programming model you deploy yourself. You own the function app. You manage the deployment, the model connection, and the infrastructure. In return, you get the full Azure Functions hosting model: event-driven triggers, Flex Consumption billing, VNet integration, managed identity, and azd-based deployment pipelines.

The decision table:

SituationUse
Need a fully managed agent host with enterprise SLAsFoundry Agent Service
Agents triggered by events, schedules, or queue messagesServerless agents runtime
Need VNet integration or custom deployment pipelinesServerless agents runtime
Want scale-to-zero billing for bursty agent workloadsServerless agents runtime
Need agents embedded in an existing function appServerless agents runtime
Prefer not to manage the agent host infrastructureFoundry Agent Service

These are not mutually exclusive. The serverless agents runtime can call tools hosted in Foundry via MCP servers, and Foundry agents can call tools hosted in Azure Functions. The two runtimes can coexist in the same architecture.

How Azure Functions Serverless Agents Runtime differs from Durable Functions

Post 4 in this series covers Durable Functions for directed agentic workflows in detail, but the short version is:

Durable Functions is for directed, deterministic workflows: you define the steps, the model executes them in order, and Durable Functions handles state, retry, and fault tolerance. The workflow is predictable.

The serverless agents runtime is for autonomous agents. You give the agent instructions and tools, and Microsoft Agent Framework determines how to use them to accomplish the goal. The execution path is not predetermined.

If your AI-driven process has fixed, ordered steps and you need auditability, use Durable Functions. If you want the agent to figure out the steps, use the serverless agents runtime.

What to know before you build

It is preview. The programming model, file format, and configuration details are subject to change. Do not build production-critical workloads on this today without a plan for the preview-to-GA migration.

It requires a Foundry project and model deployment. The azd template provisions both automatically, but you need an Azure subscription with permissions to create Foundry resources and model deployments. Some organizations have restrictions on which model deployments are permitted.

The azd template provisions real Azure resources with real costs. The Flex Consumption plan keeps costs very low for low-traffic agents, but the Foundry model deployment, Container Apps session pool, and Connector Namespace resources still incur costs. Review the Bicep templates in infra/ before running azd up in a production subscription.

Custom Python tools are how you add app-specific logic. The runtime provides the agent loop and the MCP connections. For anything that requires your own code โ€” calling internal APIs, reading proprietary data sources, applying business rules โ€” you write Python tool functions and register them in the agent definition.

Getting started

The quickstart template is the right starting point:

azd init --template Azure-Samples/functions-quickstart-serverless-agents-azd -e serverless-agents
azd env set TO_EMAIL <your-email>
azd up

Review the three configuration files in src/ before deploying. They are short and readable, and understanding them before the first deployment saves debugging time later.

The email delivery step (setting TO_EMAIL and authorizing the Microsoft 365 Outlook connection) is optional. If you skip it, the timer agent still runs and returns its digest in the final response, which you can verify in Application Insights logs.

Try it with a weather sample.

If you want to see the runtime in action with a minimal, self-contained example before committing to the full quickstart, I built a companion sample: a weather chat agent that fetches live conditions and 3-day forecasts for any location using Open-Meteo, no API key, no M365 connector, no email setup required.

The agent is defined in a single main.agent.md file. It uses Python code execution via the Container Apps session pool to call the Open-Meteo API and returns structured weather data in the chat UI. Deploy it in three commands:

git clone https://github.com/steefjan1/weather-agents
cd weather-agents
azd up

Select Central US when prompted for location โ€” the runtime is in preview and region availability is limited. The chat UI is at https://<function-app-name>.azurewebsites.net/api/agents/main/ once deployment completes.

Agent Chat UI showing a response to "What is the weather in Amsterdam?" โ€” current temperature 19ยฐC (66ยฐF), relative humidity 54%, wind speed 8.3 km/h, and a 3-day forecast showing today's high of 22.4ยฐC with 2% precipitation chance, tomorrow's high of 19.7ยฐC with 67% precipitation chance, and the day after at 19.7ยฐC with 2% precipitation chance.
The weather agent runs on the Azure Functions serverless agents runtime, pulling live conditions and a 3-day forecast for Amsterdam from Open-Meteo via sandboxed Python code execution in an Azure Container Apps dynamic session. The agent is defined in a single main.agent.md file.

The README documents two known issues you will hit if you try to build from scratch rather than the official quickstart: a broken transitive dependency in azurefunctions-agents-runtime that pins a yanked version of github-copilot-sdk, and the region constraint. Both are worth knowing before you invest time in a custom deployment.

Up next: Durable Functions as the Orchestration Layer for Directed Agentic Workflows

The Four Things Naive RAG Diagrams Leave Out

You might have seen the diagrams like four boxes, left to right, with indexing, retrieval, augmentation, and generation. Parse the PDF, chunk the text, embed the chunks, store the vectors. Then embed the question, search, stuff the results into a prompt, and generate. I have seen it circulating every few weeks with a fresh coat of branding and a caption promising an end to hallucination.

The diagram is not wrong. It is a decent first explanation of naive RAG. The problem, however, starts when someone treats it as a design. TThe version I saw recently ended its augmentation box with three words: zero hallucination guaranteed.

That claim is where I want to start, because it is the tell. Retrieval-augmented generation reduces fabrication. It does not eliminate it. Anyone promising zero has not yet run an evaluation against their own system.

So here are the four things the four-box picture leaves out, in the order they will hurt you. I have put runnable samples for each one in a companion repository.

Gap 1: Retrieval is not the same thing as vector search

Naive RAG diagrams draw a single arrow from question to embedding to vector database. That works beautifully in demos, because demo questions are written in the same register as the source documents.

Production questions are not. They contain product codes, policy numbers, abbreviations, proper nouns, and negations. Embeddings capture meaning, and a product code has no meaning to capture.

I built a small corpus to measure this rather than assert it: eight synthetic Dutch policy documents, two consecutive years of the same policy, a collective variant with a structurally identical pricing table under different codes, and a separate reglement for medical aids. Thirty-three chunks. Then eleven questions with a known correct chunk for each.

What vector-only missed

Vector-only retrieval got seven of the eleven right. The failures were not random:

  • Which discount applies to code BAS-VR-400? returned the document’s changes section, which names the code but never prices it. Right document, wrong section, and the retrieved chunk looks relevant enough to answer from.
  • Does medical acceptance apply to package AANV-CO-03 in 2026? returned the 2025 document, which says the opposite: right topic, wrong year, inverted answer.
  • How many physiotherapy treatments are in the Extra package in 2025? returned a chunk from the basic policy entirely.

That second one is the one that should worry you. It is not a near miss. The prose in the two years is nearly identical, the answer is reversed, and nothing downstream can tell. An assessor reading a fluent, cited, confidently wrong answer about acceptance criteria has no signal that anything went sideways.

Keyword search, meanwhile, nails the code lookups. Okapi BM25 has been solving this problem since before any of us had an opinion about transformers.

Worth being precise about what this does and does not prove. My first version of this corpus had three documents and eight chunks, and vector-only scored four out of five, because with eight chunks there is nothing to confuse. The gap only appears once the corpus contains things that genuinely resemble each other. If your own evaluation shows dense retrieval doing fine, check whether your test set is hard before concluding your pipeline is.

Fusion widens the pool, reranking picks the answer

Therefore, the answer is not to pick a side. Azure AI Search will run both and fuse the result lists with reciprocal rank fusion. Then a semantic reranker reorders the fused list using a cross-encoder that actually reads the query against each candidate.

Here is where my expectations were wrong, and where the measurement earned its keep. Across the eleven questions:

StrategyTop-1 correctMRR@5
Vector-only7 / 110.77
Hybrid (BM25 + vector, RRF)6 / 110.72
Hybrid + semantic reranker11 / 111.00

Adding keyword search made it worse. Hybrid lost a case that vector-only got right, and fixed none.

Why fusion alone went backwards

The reason is visible in the failures. BM25 matches the literal string BAS-VR-350, and that code appears in the document’s changes section, which names codes without pricing them. Lexical matching therefore promoted chunks that contain the code and cannot answer the question. Reciprocal rank fusion then faithfully merged two ranked lists, because RRF has no notion of whether a chunk answers anything. It fuses positions, not relevance.

The cross-encoder is what fixed it. It reads the question against each candidate and understands that a question about a discount needs the row with a price in it, not the sentence announcing that the code exists. That took the same candidate set from six correct to eleven.

So the lesson is sharper than “use hybrid search”. Hybrid retrieval widens the candidate pool; reranking is what converts a wider pool into better answers. Ship the first without the second, and you may go backward quietly, because nothing in the pipeline reports that it happened.

Diagram comparing vector-only retrieval, which returns the wrong table row, with hybrid BM25 and vector search fused by RRF and reordered by a semantic reranker, which returns the correct row.
Fusion widens the candidate pool; the cross-encoder is what turns it into a correct answer.

One clean question per turn is an assumption

Query handling is the other half of this gap. The diagram assumes one clean question per turn. Real questions arrive compound: we switched to the Compleet package in March, does my son’s dental work fall under that or under the basic policy, and does the deductible apply? That is three questions. Embed the whole sentence, and you retrieve the average of three intents, which is nothing in particular.

Agentic retrieval in Azure AI Search handles that by decomposing the query into subqueries, running them in parallel, reranking each, and merging. Extractive retrieval went generally available in API version 2026-04-01. Query planning and answer synthesis remain preview. Worth knowing which half you are depending on before you promise it to a steering committee.

Gap 2: Chunking is most of the work

“Chunk text for sharp recall.” One bullet. In practice, this single decision determines more of your answer quality than your choice of model.

Fixed-size splitting is what every quickstart does and what almost nothing should do. Run a 400-character window with 50 characters of overlap over a document containing a pricing table, and the splitter lands mid-row. This is the actual output from the sample, not an illustration:

                        | EUR 3,00          | BAS-VR-100  |
| EUR 200 | EUR 6,50 | BAS-VR-200 |
| EUR 300 | EUR 10,00 | BAS-VR-300 |
| EUR 400 | EUR 14,00 | BAS-VR-400 |
| EUR 500 | EUR 19,00 | BAS-VR-500 |

## 2. Fysiotherapie

Fysiotherapie wordt vanaf de 21e

Look at what survived. No header row, so nothing says which column is the deductible, which is the monthly discount, and which is the product code. The first row is cut mid-cell: its deductible tier is gone, leaving a discount attached to nothing. No document title, so nothing says this is the 2026 basic policy rather than the 2025 one or the collective variant, all three of which carry a table of exactly this shape with different numbers. And the chunk runs on into an unrelated section about physiotherapy, ending mid-sentence.

Retrieve that, and the model has to guess. It will guess. It will sound certain. And the citation attached to it will make the wrong answer more credible, not less.

Diagram showing a fixed-size splitter cutting a pricing table in half so one chunk holds rows without a header, next to structure-aware chunking that splits on headings and keeps the table intact.
The headerless-table count is a defect count. Each one can produce a confident wrong answer about a product code.

What structure-aware chunking does differently

The sample runs three chunkers over the same document and counts how many chunks ended up holding table rows with no header. Fixed-size produces one out of three. Recursive paragraph splitting produces none, but leaves every chunk without a section heading. Structure-aware produces four chunks, none headerless, none context-free.

The difference is three rules, and none of them is clever: split on headings rather than character counts, never split a table, and prepend the document title and section heading to every chunk so an isolated chunk still says what it is.

That last rule is what makes the three near-identical pricing tables in this corpus distinguishable at all. Without it, retrieval has to tell them apart on the numbers alone.

Gap 3: Retrieval without authorization is a breach with a chat interface

This is the gap that should worry you most, and it is absent from every version of the diagram I have seen.

Put every document in one index. Wire up a chat interface. Now every user can reach every document, because semantic search does not know about your authorization model. The retrieval layer will happily surface an internal work instruction, an HR file, or a legal memo to whoever asks a question shaped roughly like its contents.

The filter is a query construct, not a prompt instruction

Two things follow. First, the filter is a server-side query construct, not a prompt instruction. Telling the model “only use documents the user may see” is not a control; it is a suggestion to a system that has already been handed the text. Second, the filter must derive from validated token claims, never from anything the user typed.

Diagram showing a single unfiltered index returning a restricted work instruction to a customer service agent, and a corrected pipeline where an OData filter built from validated token claims trims results before the model sees them.
The filter belongs in the query. An instruction in the system prompt is a suggestion to a model that already has the text.

Two mechanisms, and the one that fails open

Azure AI Search gives you two mechanisms. The durable one is an explicit filterable collection of group identifiers on each document plus an OData filter built from the caller’s claims, which works today on the stable API and which you own end to end. The managed one ingests RBAC scopes, ACLs, or Purview sensitivity labels alongside the content and enforces them at query time when you pass the user’s token in the x-ms-query-source-authorization header.

The managed route has a sharp edge worth memorizing. If the knowledge source was created without ingestionPermissionOptions, the index holds no permission metadata, and results come back unfiltered regardless of the header. It fails open quietly, and the only way to fix it is to recreate the knowledge source. As of the current GA release, document-level permissions on indexed sources remain in preview.

Whichever you choose, write the leak test. The sample repository includes one: a query, an unauthorized caller, and an assertion that fails the build if the restricted document comes back. Twelve lines. Run it in CI.

Gap 4: “Zero hallucination” is a claim, and claims get measured

Grounding the prompt does not guarantee a grounded answer. Three failure modes survive the diagram intact.

The model can prefer what it already knows over what you retrieved. Ask about a monthly premium that appears nowhere in your corpus, and a model trained on the open internet has plausible Dutch premiums available. It will produce one.

The model can blend two chunks into a claim neither of them makes. This is the subtle one, because every individual fact traces back to a source.

And the model can answer confidently when retrieval returned nothing relevant at all, because nothing in the naive RAG pipeline tells it that “I do not know” is an available output.

Diagram of three hallucination modes that survive naive RAG โ€” parametric leakage, blended claims, and no refusal path โ€” alongside a corrected pipeline with citation-enforced prompting and a judge producing four scores.
Refusal rate is the number that separates grounded from fluent.

Three rules that make it measurable

Consequently, the fix is threefold and unexciting: an explicit refusal string in the prompt so refusal is detectable rather than inferred, mandatory citation of a reference identifier after every claim so each statement is checkable, and an evaluation set that contains questions your corpus cannot answer.

That last point is the one most teams skip. Everyone builds a golden set of questions the documents answer well. Almost nobody includes the withdrawn product code, the topic that was never documented, or the answer that lives in a file the user may not see. Those are exactly the cases that generate the incident report.

Score retrieval and generation separately, too. A wrong answer with good retrieval is a generation problem. A wrong answer with bad retrieval is a retrieval problem. Without both numbers, you will spend a week tuning the wrong half of the system.

What the numbers actually said

Then the result, which surprised me: fourteen out of fourteen on groundedness and valid citations, and five out of five refusals on the unanswerable questions. The model corrected false premises rather than accepting them; asked whether medical acceptance applied in 2026, it answered yes and cited; asked about a product code withdrawn before the corpus begins, it refused outright rather than interpolating a plausible price from the neighboring rows.

Terminal output of an evaluation run over fourteen questions: nine answered with citations, five refused, scoring 14/14 on groundedness, citation validity and retrieval hit, and 5/5 on refusals.
The point of gap 4 is that this output exists at all. An ungrounded system produces no such table, only fluent answers and no way to tell.

I want to be careful about what that number means, because it is easy to oversell in the other direction. Retrieval hit fourteen out of fourteen in the same run, so generation was working from good material throughout. This is not evidence that hallucination is solved. It is evidence that the three unexciting mechanisms above are sufficient when retrieval is doing its job, which is the argument for building all four gaps rather than any one of them.

One caveat I would want stated if someone showed me this number: the judge was the same model as the generator. A model scoring its own output shares its own blind spots, and the groundedness figure is inflated to an unknown degree by that. Use a different model for judging if the number needs to carry weight.

Where this is the wrong answer

If your corpus is fifty pages of prose, with no product codes, no tables, one audience, and low stakes, then the four-box diagram is enough. Hybrid retrieval, structure-aware chunking, security trimming, and a groundedness harness are all overhead you do not need yet. Build the simple thing, ship it, and see what breaks.

The four gaps become urgent at specific, recognizable moments: the first exact-match question that returns the wrong table row, the first document that should not be visible to everyone, and the first stakeholder who asks how you know the answers are right. If none of those have happened, you are fine.

The part the diagram gets right

Context beats prompt engineering. That much is true, and it is the reason the picture keeps circulating. But “give the model the right information at the right time” restates the problem. It is not a solution. The right information depends on hybrid retrieval and reranking. The right time depends on query decomposition. Whether the user was entitled to that information depends on trimming. And whether the model actually used it depends on evaluation.

Do all four and the numbers hold up โ€” mine did. Skip any one of them, and you will not find out which one you skipped until someone asks a question about last year’s policy and gets this year’s answer.

Four boxes, four gaps. The gaps are where the engineering lives.

The samples are at steefjan1/naive-rag-gap: hybrid retrieval and reranking, chunking, security trimming, and groundedness evaluation, provisioned with a single azd up. Four of the five run end to end against a live service; the agentic retrieval sample needs a knowledge base that none of the scripts create, so treat that one as a sketch rather than a worked example.

They target Azure AI Search API versions current as of August 2026. Agentic retrieval and document-level permissions are both moving quickly, so check the docs before assuming a preview flag is still a preview flag.

Citadel Grew Up: What the citadel-v1 Release Means If You Built on the AI Hub Gateway.

The Citadel Governance Hub accelerator that sits underneath my entire five-part Citadel Platform series just had a significant release. In addition, the citadel-v1 branch of the AI Hub Gateway Solution Accelerator repositions the project from “a solid APIM gateway pattern” to the official reference implementation of Layer 1 in Microsoft’s AI Citadel Blueprint.

I cloned the branch and went through it with one question in mind: what does this change for anyone who, like me, deployed and built on the earlier iteration? The answer starts with one finding. As a practitioner, the whole point of this blog is honesty: the API surface I used throughout the series is now explicitly labeled legacy.

Note that citadel-v1 has not yet been merged to main; if you deployed from the main branch without specifying --branch citadel-v1, you are on the earlier architecture.

Let’s start with the bigger picture, then get to that.

The 4-layer AI Citadel Blueprint

The README now frames the accelerator as one layer of a larger architecture. The AI Citadel Blueprint describes four interlocking layers, each with its own responsibility and implementation:

  • Layer 1, the Governance Hub, is this accelerator: runtime enforcement through a unified AI gateway, policy-as-code, identity validation, token rate limiting, content filtering, and cost attributionโ€”everything my series built and tested lives in this layer.
  • Next, layer 2, AI Control Plane, covers the agent runtime, observability, and compliance: agent traces, AI evaluations, and fleet operations, implemented through the Microsoft Foundry control plane.
  • Subsequently, layer 3, Agent Identity, handles agent identity and lifecycle governance through Agent 365: unique agent identities, blueprints, shadow agent detection, and a sponsorship model.
  • And finally, layer 4, the Security Fabric, provides unified protection through Microsoft Defender for AI threat intelligence, Purview for data governance, and Entra for authentication and authorization.
Stack diagram of the four AI Citadel Blueprint layers: Security Fabric, Agent Identity, AI Control Plane,, and Governance Hub at the base. A bracket marks Layer 1 and part of Layer 2 as covered by the Citadel blog series, with Layers 3 and 4 marked not yet explored.
The four layers of the AI Citadel Blueprint, with the series’ coverage marked: Layer 1 fully, Layer 2 partially through the registry work.

Looking back at the series through this lens, my five posts covered Layer 1 thoroughly, and the registry work with Azure API Center reached into Layer 2 territory before the layer had that name. The kill switch from Part 4 sits squarely in Layer 1 as runtime enforcement. What the series never touched, and what I now have vocabulary for, is Layers 3 and 4. That’s useful: it turns “what’s missing from my platform” from a vague feeling into a named checklist.

What citadel-v1 Changes in the AI Hub Gateway: The API Surface

Here’s the finding that matters most if you followed the series. The new LLM Access Guide defines three API surfaces on the gateway, and it’s blunt about which one you should use.

The Azure OpenAI API surface, at /openai/deployments/{deployment-id}/*, preserves the exact URL shape the Azure OpenAI SDK expects. This is what Part 2 of my series wired the weather agent against, and it’s what every code sample in the series uses. The guide now labels it “legacy integration only,” for existing code that pins that URL shape. Not the target state for new work.

The Universal LLM API, at /models/*, exposes a clean OpenAI v1-compatible surface across many models and providers through a single stable path.

The Unified AI API, at /unified-ai/*, is the recommended surface: a single wildcard endpoint that serves OpenAI-compatible calls and every provider-native pattern with dynamic routing behind it.

LLM ACCESS guide

The citadel-v1 branch documents these three surfaces in its LLM access guide. Check the guides folder in the branch for the current filename, as the documentation is actively evolving.

Comparison of three API surfaces in citadel-v1: the Azure OpenAI API at /openai/deployments marked legacy, the Universal LLM API at /models as a valid alternative, and the Unified AI API at /unified-ai marked recommended. A dashed arrow shows the migration path from the legacy surface to the recommended one.
Three API surfaces on the citadel-v1 gateway. The path my series used is now labeled legacy; nothing broke, but the arrow points one way.

I want to be precise about what this does and doesn’t mean. Nothing broke. Code targeting /openai/deployments/... keeps working, and the surface exists precisely because migrations take time. But the arrow points one way: new integrations should target /unified-ai/v1/*, and my series should be read with that footnote attached. If I started the series today, Part 2 would look different.

This doesn’t invalidate the architectural argument, and I’d argue it strengthens it. The reason the series routed the standard OpenAI SDK through APIM was to keep every call on a governed path. The Unified AI API is that same principle with a better front door: one endpoint, every provider, every pattern, all governed. The lesson survived the release; only the URL changed. Citadel is evolving fast, and this is what evolving looks like from the inside.

Contract-driven everything

The second big theme in citadel-v1 is contracts, and if you read my registry post about the AI Publish Contract, this will feel familiar in the best way.

The accelerator now ships a Citadel Access Contract package: declarative, version-controlled .bicepparam files that onboard an AI use case end-to-end. One contract deployment creates the APIM product (with naming like LLM-Healthcare-PatientAssistant-DEV), the subscription with its key, optional Key Vault secret storage, and optionally an APIM connection for Microsoft Foundry agents. I described this as a pattern worth building in the registry post, and the access contract is now live while the publish contract remains upcoming in the current release.

Alongside it sits a backend onboarding contract (llmBackendConfig) for declaratively registering LLM backends, and the whole thing is versioned through a release.json manifest at the repository root. That manifest is worth a moment of appreciation: instead of one monolithic version number, it tracks independent, component-scoped versions for the routing logic, the backend contract shape, the access contract shape, and the usage ingestion pipeline. A change to routing doesn’t force a re-version of contracts that didn’t change. That’s a small design decision that signals the project expects to be operated, not just deployed once.

The parallel to the AI Publish Contract from my registry post is direct. Both encode the same conviction: onboarding an AI workload should be a reviewed, versioned artifact in a repository, not a sequence of portal clicks someone half-remembers. The access contract governs how a workload reaches the gateway. The publish contract governs registration and description. A mature platform wants both.

Multi-provider routing, briefly

The gateway is no longer an Azure OpenAI front door with ambitions. AWS Bedrock, Google Gemini, and Anthropic Claude are first-class citizens, each available through OpenAI-compatible access, provider-native access, or both.

The design that makes this work without chaos is a fragment-based routing architecture, and one detail from the onboarding guide shows how much operational scar tissue is encoded in it. Every API type declares its own compatible pool types, and the Universal LLM API restricts pool selection to OpenAI-compatible pools before backend selection runs. Why? Because if the same model ID is registered against both a native Bedrock pool and an OpenAI-compatible one, a naive router could send an unrewritten OpenAI-shaped path to the native provider, which answers with something as friendly as com.amazon.coral.service#UnknownOperationException. The guide documents the failure mode by name. Someone hit that error so you don’t have to, which is exactly what a good accelerator encodes.

For a platform team, the practical consequence is real: model choice becomes a routing decision instead of an architecture decision. Adding Claude or Gemini to an estate governed by the hub doesn’t create a second governance perimeter. It adds a backend behind the one you already operate.

What I’d do differently starting today

Distilling this into advice for anyone deploying now:

Target the Unified AI API from day one. Start at /unified-ai/v1/* with the standard OpenAI SDK. You get the same governed path my series argued for, plus provider reach and a native-access upgrade path you’ll eventually want.

  • Adopt the access contract instead of hand-rolling onboarding. The .bicepparam contract per use case gives you reviewable, repeatable onboarding with product, subscription, and secrets in one deployment. I built a weaker version of this by hand during the series; you don’t have to.
  • Pin your contract versions consciously. release.json gives you independent version tracks. Treat contract shape changes as reviewable events in your own repo, the same way you’d treat an API schema change.
  • Look at the PII blocking mode. The PII framework now supports managed identity authentication to the Language Services, regex pre-processing before NLP detection, and a strict mode that rejects requests containing PII with a 400 instead of masking. For regulated industries, that hard-fail option changes the compliance conversation: some data should never reach the model, masked or not.

What’s next

The obvious follow-up experiment: migrating the weather agent from the legacy /openai/deployments/... path to the Unified AI API, documenting whatever breaks along the way. If the routing architecture delivers on its promise, that migration should be a base-URL change. If it isn’t, that’s a post worth writing too.

The accelerator that started this series as a useful pattern is now the reference implementation of a named layer in a published blueprint, with contracts, multi-provider routing, and a defined seam toward agent-runtime governance. Preview or not, the direction is clear, and it’s the direction the series has been arguing for all along: one governed front door, everything registered, nothing invisible.

If you’ve deployed citadel-v1 or migrated from the earlier iteration, I’d like to hear what surprised you.

Azure Governance and Identity for Integration Architects

In the Azure PaaS map post, governance and identity got a paragraph and a firm line: managed identity everywhere, secrets in Key Vault, posture visible. That paragraph is the one most PaaS write-ups skip; they cover compute, messaging, and data, then wave at security on the way out.

For an integration platform in a regulated industry, though, this layer isn’t the afterthought. It’s the part that decides whether the thing can run at all. So this post gives it the space the map couldn’t. Azure governance and identity are where a proof of concept becomes something an auditor will sign off on, and where many otherwise good architectures quietly fail their first compliance review.

The two jobs this layer does

Governance and identity sounds like one concern. In practice it’s two, and keeping them apart makes the design clearer.

Diagram splitting the layer into two columns. Identity answers "who" and contains Microsoft Entra ID, managed identities, and RBAC role assignments. Governance answers "whether and how" and contains Azure Policy, Defender for Cloud, and the audit trail. A note reads that both are always required.
Identity answers “who”: Entra ID, managed identities, RBAC. Governance answers “whether and how”: policy, D\defender, the audit trail. Identity without governance is secure but unprovable; governance without identity is documented but wide open. A platform needs both.

Identity answers who. Who is this caller? What are they allowed to reach? This is Entra ID, managed identities, and role assignments the machinery that authenticates and authorizes every hop in the platform.

Governance answers whether and how. Is this configuration allowed? Can we prove it stayed allowed? This is Azure Policy, Defender for Cloud, and the audit trail the machinery that constrains what the platform can be and evidences it after the fact.

An integration platform needs both. Identity without governance gives you a secure platform you can’t prove is secure. Governance without identity gives you a well-documented platform anyone can walk into. So let’s take each in turn, then the compliance frame that ties them together.

Identity: managed identity as the default

Here’s the single most important rule in this layer. Every service authenticates as itself, with no secret in configuration. That’s what managed identity gives you, and it’s the foundation everything else sits on.

Without it, you’re back to connection strings and API keys scattered across app settings and config files. Each one is a secret that can leak, expire, or get committed to a repo by accident. With managed identity, the platform issues each service an identity in Entra ID, and that identity authenticates directly to Key Vault, SQL, Service Bus, and Storage. No secret changes hands. Nothing to rotate, nothing to leak.

A few design points that matter for integration specifically:

  • System-assigned or user-assigned? A system-assigned identity is tied to one resource and dies with it. A user-assigned identity is a standalone resource you attach to many. For an integration platform with several services that need the same access, a user-assigned identity is usually cleaner: you grant permissions once and attach it everywhere, rather than managing a dozen separate grants.
  • Least privilege, per service. Managed identity makes authentication clean, but authorisation is still on you. Each service should hold exactly the roles it needs and nothing more. The integration service that reads from Service Bus doesn’t need write access to the whole storage account. So scope the role assignments tightly, because a broad grant is a standing risk long after anyone remembers making it.
  • RBAC over access policies. Where a service supports Azure RBAC for its data plane, Key Vault now prefers it over the older per-resource access policy model. RBAC gives you one consistent permission model across the platform, which is far easier to audit than a patchwork of resource-specific policies.

Identity: Entra ID and the human side

Managed identity handles service-to-service. Entra ID also governs the human and external edges of the platform.

For inbound calls, Entra ID handles authentication and authorisation validating tokens, enforcing scopes, and applying conditional access where the risk warrants it. App Service and API Management both integrate with it directly, so the platform can offload the whole OIDC flow rather than hand-rolling token validation. That’s less code and, more to the point, less code to get wrong.

The honest note: Entra ID’s built-in flows are excellent for standard cases and awkward for unusual ones. If your authorization logic is genuinely complex, with per-tenant rules, dynamic scopes, and fine-grained resource permissions, understand where the platform’s built-in handling stops and your own logic has to begin. Leaning on Easy Auth for something it wasn’t designed for is a common way to end up with authorization gaps you don’t discover until an audit.

Secrets: Key Vault for what can’t be an identity

Managed identity removes most secrets. It doesn’t remove all of them. Third-party API keys, certificates, signing keys these can’t be a managed identity, so they need somewhere safe to live. That’s Key Vault.

Flow diagram. An integration service authenticates as its user-assigned managed identity in Entra ID. That identity reaches SQL, Service Bus, and Storage directly with no secret changing hands. For third-party API keys and certificates that can't be a managed identity, the service reads them from Key Vault at runtime. A note explains that rotating a secret becomes a Key Vault operation rather than a redeploy, and every read is logged.
A service authenticates as its Entra ID-managed identity, reaching SQL, Service Bus, and Storage with no secrets changing hands. For the few secrets that can’t be an identity-based third-party key or certificate, it reads them from Key Vault at runtime using the same identity.

The pattern is straightforward. Secrets live in Key Vault, and services read them at runtime using their managed identity. No secret sits in configuration; the app holds a reference, and the platform resolves it. As a result, rotating a secret is a Key Vault operation, not a redeploy.

For a regulated integration platform, Key Vault also stores the certificates that underpin private connectivity and signing, and it provides an access log of every secret read, which matters more than it sounds, because “who accessed this key and when” is a question auditors actually ask.

Governance: policy, posture, and the audit trail

Identity secures the platform. Governance proves it and constrains it so it can’t drift out of compliance.

Azure Policy enforces the guardrails. Policy lets you assert rules across the platform and block or flag anything that violates them. No public endpoints. Encryption required. Approved regions only, which matters directly under data-residency rules. Policy is what stops a well-meaning change from quietly breaking a compliance requirement, because it refuses the change rather than trusting everyone to remember the rule.

Defender for Cloud gives you posture. It surfaces misconfigurations, missing controls, and active threats across the platform, and scores you against benchmarks. For an integration platform touching regulated data, that continuous posture view is the difference between finding a gap yourself and having an auditor find it for you.

The audit trail evidences everything. Azure Monitor and the activity log record what changed, who changed it, and when. Under most compliance regimes, you don’t just have to be secure; you have to prove you were secure, continuously, over time. The audit trail is that proof. So wire it in from the start, because you can’t reconstruct an audit trail you didn’t capture.

The compliance frame: why this layer is non-negotiable

Everything above applies to any serious platform. In a regulated Dutch healthcare context, though, the frame is sharper, and it’s worth naming the constraints that turn best practices into requirements.

Under the AVG, the Dutch implementation of the GDPR imposes strict obligations on the handling, minimization, and residency of personal data. Approved-region policy, tight role scoping, and the access trail stop being good hygiene and become the evidence you’re meeting those obligations. NEN 7510, the Dutch standard for information security in healthcare, adds specific controls around access management and traceability that map almost directly onto Entra ID role assignments and the audit trail. DORA brings operational-resilience and third-party-risk requirements that lean on the same posture and logging foundations. And the EU AI Act, where the platform touches AI workloads, layers on transparency and oversight duties that again rest on identity and audit.

The through-line: these regimes don’t ask for exotic new machinery. They ask you to apply identity, policy, and audit rigorously, and to prove it. So the governance layer isn’t compliance overhead bolted onto the architecture. Done right, it is the compliance posture, expressed as configuration.

Where this layer gets over-applied

Consistent with the series the honesty section. Rigour in this layer is right; ceremony for its own sake is not.

  • Not every secret needs a Key Vault reference if it isn’t a secret. A non-sensitive configuration value doesn’t belong in Key Vault just because everything else does. Reserve it for things that actually need protecting, or you bury the real secrets in noise.
  • Not every workload needs the strictest tier of every control. A platform handling public reference data doesn’t need the same isolation and scrutiny as one touching patient records. Match the rigour to the data classification, rather than applying maximum controls uniformly and paying for it in friction everywhere.
  • Policy that only flags is policy that gets ignored. A pile of advisory policies nobody acts on is worse than a few enforced ones, because it creates the appearance of governance without the substance. Enforce what matters; don’t drown the signal.

The shape of it

For an integration architect, Azure governance and identity are the layer that decides whether the platform is allowed to exist, not just whether it works. Managed identity removes the secrets. Entra ID governs who gets in. Key Vault holds what’s left. Policy constrains the platform, Defender watches it, and the audit trail proves it continuously, the way every regime from AVG to the EU AI Act actually demands. Get this layer right, and compliance stops being a gate you dread. It becomes a property the architecture already has.

Want the layer this sits inside? The Azure PaaS map puts governance in context against compute, integration, and data, and walks the five-question framework across all of them.

Managed Identity in Logic Apps Standard: A Zero Trust Read

For years, Managed Identity has been the easy part of a Zero Trust story on Azure right up until a developer opened VS Code. Deployed Logic Apps could already authenticate to Entra-protected resources with no secrets in sight. The local dev loop, however, couldn’t. You’d wire up a connection string or a local key to run and debug, then swap it out for Managed Identity before deployment. Two auth models, one workflow, and a seam that every “we don’t store secrets” policy quietly depended on someone remembering to close.

Wagner Silveira’s TechCommunity walkthrough, Use connectors with Managed Identity in the Logic Apps Standard extension, covers an update that closes that seam. You can now build connectors โ€” both Azure managed connectors and service provider connectors โ€” that use Managed Identity as the authentication parameter, and run them locally while you develop and debug. Locally, the extension authenticates as your signed-in developer identity through the Azure default credential pattern. After deployment, that same connection authenticates as the app’s managed identity instead. In other words: same connector definition, same auth model, just a different identity behind it depending on where it runs.

Why this is a Zero Trust story, not just a DX improvement

It’s tempting to file this under developer experience and move on. I’d argue it belongs in the identity and access conversation instead.

Zero Trust asks for consistent, identity-based enforcement everywhere, not only in production, where the compliance team is watching, but in every environment a workload touches. The dev-to-prod credential swap was a structural exception to that principle: a place where the enforced pattern was “do it properly,” and the practiced pattern was “do it however runs today.” Local .env files and connection strings scoped to a developer’s convenience tend to outlive the sprint that created them. As a result, it’s usually the dev environment, not prod, where stray credentials pile up quietly, invisible to your access reviews.

Collapsing local and deployed auth onto the same Managed Identity model doesn’t just remove secrets from one more service. More importantly, it removes the exception itself. There’s no longer a version of “getting this connector running” that skips Entra-issued, RBAC-governed identity. That’s the real Zero Trust gain here: not that a secret disappeared, but that a place secrets used to hide no longer exists.

Where this doesn’t do the work for you

Managed Identity is authentication, not authorization, and this update doesn’t change that. It gets you a verified identity at the door; it says nothing about what that identity is allowed to touch once it’s inside. Silveira is direct about this in his post; if a connection throws an authorization error, check the RBAC role assignment on the target resource first, because Managed Identity will happily authenticate an identity that’s been granted far more access than the workflow actually needs.

In practice, that means the credential-less win is only as good as the RBAC discipline behind it. An identity with Contributor on a resource group, because nobody wanted to think about scoping, isn’t meaningfully more “Zero Trust” than a connection string sitting in a config file; it’s just a differently shaped version of too much trust. If you’re rolling this out, the RBAC assignment deserves the design review; the connector configuration doesn’t.

There’s also a smaller, practical catch worth knowing before you build on this: the Managed Identity path for managed connectors doesn’t populate dynamic values in the designer. You lose the friendly dropdowns and supply values manually instead. It’s a minor friction, but it’ll surprise the first person on your team who hits it mid-demo.

Try it yourself

I’ve put together a small sample project, a Logic Apps Standard workflow that lists blobs on a schedule, authenticated with Managed Identity both locally and once deployed, plus Bicep that provisions the whole thing end to end (including a role assignment scoped to exactly one storage account, not the resource group). azd up gets you a running workflow; no manual portal clicking required.

Grab it from the sample repo (link once published) and run:

azd auth login
azd up

One setting made the difference between “deploys clean” and “actually authenticates”: WORKFLOWS_AUTHENTICATION_METHOD, set to managedServiceIdentity on the deployed app. It’s easy to miss; I did, in an earlier version of this sample, because the connection, the access policy, and the RBAC role assignment can all be independently correct, and the workflow will still fail at runtime with a bare Key 'token' not found in connection profile until that setting is in place. If you deploy this yourself and hit that exact error, that’s almost certainly why.

The governance question this actually raises

Platform and integration teams have mostly answered “can we authenticate without secrets here?” The harder question left standing is: do we, consistently, everywhere, including local dev? This release removes the last technical excuse for Logic Apps Standard. What’s left is a policy and habit question. Does your team’s definition of “done” for a new connector include verifying it never touched a stored credential, in any environment? Or does that check still only happen at the production gate?

Worth asking before the next connector goes into a workflow, not after.

Further reading: Wagner Silveira’s original walkthrough, Use connectors with Managed Identity in the Logic Apps Standard extension, on the Azure Integration Services Blog.

Azure Data Patterns for Integration Architects

In the Azure PaaS map post, the data layer got one paragraph and a rule: pick by access pattern, not by which service feels modern. That rule holds. But it’s also where most write-ups stop: SQL for relational integrity, Cosmos DB for scale, and Redis in front; for an integration architect, that’s the least interesting part of the story.

The interesting part is what the data layer has to do that’s specific to integration. Messages arrive twice. Workflows run for hours and need somewhere to keep their state. A write to your database and a publish to a queue have to succeed or fail together. So this post skips the service comparison and covers the patterns instead. Azure data patterns for integration are less about which store you pick and more about how you use it.

Why integration data is different

A typical application owns its data. It writes, it reads, it controls the whole path. Integration doesn’t work that way. Instead, integration sits between systems it doesn’t own, reacting to events it didn’t originate, and it has to stay correct when those systems misbehave.

That changes what the data layer is for. It’s no longer just persistence. It becomes the place where you enforce correctness that the messaging layer can’t guarantee on its own. Three patterns come up again and again. Let’s take them in turn.

Pattern 1: Idempotency stores

Here’s the problem. At-least-once delivery is the norm for most messaging systems, including Service Bus. So the same message can arrive twice after a retry, a redelivery, or a consumer crash-and-restart. Process it twice, and you’ve charged the card twice or created two orders. That’s not a rare edge case. In a busy integration platform, it’s a Tuesday.

Flow diagram of an idempotency store. A message that may arrive twice reaches a decision: has this ID been seen before? The check queries an idempotency store in Cosmos DB or Redis, keyed by ID with a TTL. If yes, the handler skips the duplicate. If no, it records the ID and processes the message once.
At-least-once delivery means the same message can arrive twice. Check the ID against a store first; skip if seen; record and process once if not.

The fix is an idempotency store. Before you process a message, you check whether you’ve seen its ID before. If you have, you skip it. If you haven’t, you record the ID and proceed. As a result, duplicate deliveries become harmless.

The design questions that matter:

  • Where does the key come from? Ideally, the source system supplies a stable business key, an order ID, and a transaction reference. Failing that, a hash of the message content works, though it’s more fragile.
  • Where do you store it? This is a high-frequency, low-latency lookup on a single key. Therefore Cosmos DB or Redis fit well, and a relational table works too if the volume is modest. The access pattern points at the store, exactly as the map post argued.
  • How long do you keep it? Retention has to outlast the longest possible redelivery window. Too short, and a late duplicate slips through. So set a TTL that comfortably exceeds your retry and dead-letter timelines, then expire old keys automatically.

The honest note: idempotency at the store isn’t the same as an idempotent operation. If the downstream side effect isn’t itself safe to repeat, the store only narrows the window; it doesn’t close it. Design the operation to tolerate retries wherever you can.

Pattern 2: The outbox pattern

This one solves the dual-write problem, and the dual-write problem is subtle enough that plenty of teams ship it broken.

Picture a handler that does two things. It writes a record to the database, and it publishes an event to a queue. Both must happen, or neither. But they’re two separate systems, so there’s no shared transaction. Write succeeds, publish fails; now the database and the downstream world disagree. Publish succeeds, write fails; now you’ve announced something that didn’t happen.

Diagram of the outbox pattern. A handler writes a business record and an outbox row inside one dashed database transaction boundary, so they commit together. A publisher tails the outbox, marks each row done, and sends to a broker and consumer with at-least-once delivery. A callout notes the consumer therefore needs an idempotency store.
The business record and the outbox event commit in a single database transaction, so they succeed together. A separate publisher then tails the outbox and publishes each event, giving at-least-once delivery downstream.

The outbox pattern closes the gap. Instead of publishing directly, you write the event into an “outbox” table in the same database transaction as your business record. Because they share one transaction, they commit together or not at all. Then a separate process reads the outbox and publishes the events, marking each one done as it goes.

A few things fall out of this design:

  • The database becomes the source of truth for what should be published. If the publisher crashes mid-run, it restarts and picks up where it left off. Nothing is lost, because nothing left the database until it was safely committed.
  • Publishing becomes at-least-once. The publisher might send an event, crash before marking it done, and send it again on restart. So the consumer on the other end needs, you guessed it, an idempotency store. The two patterns work together.
  • A change-feed makes it cleaner. Cosmos DB’s change feed, or a similar mechanism, lets the publisher tail committed changes rather than poll a table. That reduces latency and load, though a simple polling publisher is perfectly fine to start.

The trade-off is honest latency. The outbox adds a hop between commit and publish. For most integration workloads that’s a few seconds at most, and well worth it for the correctness guarantee. But if you need genuinely instant propagation, the outbox isn’t your pattern.

Pattern 3: State for long-running workflows

Synchronous request-response keeps its state in memory for the length of a call. Integration workflows don’t have that luxury. A process can span minutes, hours, or days while waiting for approval, a batch window, or an external callback. That state has to live somewhere durable, because the compute running it will scale, restart, and move underneath it.

So where does workflow state go? It depends on who’s orchestrating.

Diagram of state handling for long-running workflows. A workflow instance branches on who orchestrates. Engine-managed state covers Logic Apps, which uses its own runtime store, and Durable Functions, which uses a storage backend. Hand-rolled orchestration keeps state in an explicit Cosmos DB store, one document per instance. A band across the bottom shows the correlation ID, stored with the instance and carried on every outbound call, which matches an external callback back to the right in-flight instance.
The compute running a workflow scales, restarts, and moves, so the state has to live somewhere durable. A workflow engine manages it for you; hand-rolled orchestration needs an explicit store. Either way, a correlation ID reconnects a callback to the right in-flight instance.
  • Logic Apps and Durable Functions manage their own state: Both persist workflow state for you; that’s a large part of why they exist. Durable Functions keeps it in a storage backend; Standard Logic Apps keeps it in its own runtime store. In these cases, you rarely touch the state directly, but you should know it’s there and know that it’s what makes the workflow survive a restart.
  • Hand-rolled orchestration needs an explicit store: When you’re coordinating steps in your own code rather than a workflow engine, you own the state. A document store like Cosmos DB fits well here: one document per workflow instance, updated as the process advances through its steps. The flexible schema helps, because a workflow’s state shape often evolves as you add steps.
  • Correlation is the piece people forget: Long-running workflows wait for things to come back, and when a callback arrives, you have to match it to the right in-flight instance. That means a correlation ID, stored with the instance and carried on every outbound call. Without it, you have durable state you can’t reconnect to the event that needs it.

Where these patterns are the wrong answer

Consistent with the rest of the series, the honesty section. Patterns solve problems, and applying them where the problem doesn’t exist adds cost.

  • Skip the idempotency store when the operation is naturally idempotent: Setting a status to “shipped” twice changes nothing. If every side effect is already safe to repeat, a dedup store is machinery you don’t need.
  • Skip the outbox when you don’t dual-write: If a handler only writes to the database, or only publishes, there’s no gap to close. The outbox earns its keep specifically when one commit must produce one publish.
  • Skip explicit state stores when a workflow engine already owns the state: Standing up your own Cosmos-backed state store next to Durable Functions duplicates what the runtime already gives you. Reach for the explicit store only when you’re orchestrating by hand.

The shape of it

For an integration architect, the data layer isn’t mainly a choice between SQL and Cosmos. That choice matters, but the access pattern usually makes it for you. The real work is the patterns that keep an integration platform correct when systems it doesn’t control misbehave. So an idempotency store absorbs duplicate deliveries. An outbox makes both a write and a publish succeed. A durable state store lets a workflow outlive the compute running it. Get those right, and the underlying store SQL, Cosmos, and Redis become implementation details rather than the headline.

Want the layer this sits inside? The Azure PaaS map puts data in context against compute, integration, and governance, and walks the five-question framework across all of them. And the messaging and orchestration post covers the delivery guarantees these patterns lean on.

Token Economics in Practice: What the Citadel Cost Attribution Policy Actually Meters

The FinOps Foundation published a piece called Token Economics: The Atomic Unit of AI Value, and it’s one of the better attempts I’ve seen at giving AI cost management a real vocabulary. Tokens as the atomic unit of cost, goodput instead of raw throughput, and a warning that the token meter is increasingly hidden inside SaaS subscriptions you don’t control.

Most writing on this topic stays theoretical. I have something to test it against. The Citadel Platform series on this blog built cost attribution and semantic caching into a real APIM gateway, running in Sweden Central, metering a real agent. So instead of summarizing the FinOps article, this post uses it as a checklist. Where does the Citadel implementation already deliver on token economics, and where does it fall short?

The honest answer: it holds up well on attribution and caching, and it has clear gaps on goodput, yield, and routing. Let’s go through it.

Scorecard table mapping six token economics concepts to Citadel implementation status. Cost attribution, semantic caching, and gateway meter visibility are implemented. Goodput tracking and model routing are gaps. Token yield rate has the data in Cosmos DB but no outcome tagging yet.
The scorecard up front: where the Citadel Hub delivers on token economics today, and where the honest gaps are.

Three token economics ideas worth carrying forward

I’ll paraphrase the three concepts I’m testing against, and you should read the original for the full argument.

Goodput, not throughput. Raw token volume tells you what you spent, not what you got. Goodput asks how many of those tokens produced useful output within acceptable latency. A retry storm and a productive session can burn the same token count.

The cost stack extends beyond the token. Tokens are the atomic unit, but the bill includes orchestration overhead, retries, tool-calling scaffolding, and increasingly, SaaS subscriptions that embed token consumption behind a flat price. When the meter sits inside someone else’s product, you lose the visibility FinOps depends on.

Engineering levers matter more than procurement levers. Model routing, semantic caching, and compressing tool-calling overhead move the cost curve more than negotiating a discount does. The FinOps article cites Cloudflare’s Code Mode work, which cut MCP tool-schema token overhead dramatically by changing how tools present themselves to the model.

Now let’s hold the Citadel Hub against those three ideas.

What the Citadel Hub already meters

Every call the weather agent makes flows through apim-wpvlimv4ngkns, and two of the five governance policies from earlier in the series do the token economics work.

The cost attribution policy emits token metrics per call, dimensioned by subscription and agent:

xml

<azure-openai-emit-token-metric namespace="citadel">
<dimension name="Subscription ID" />
<dimension name="Agent ID" value="@(context.Request.Headers.GetValueOrDefault("X-Agent-Id", "unknown"))" />
<dimension name="API ID" />
</azure-openai-emit-token-metric>

That gives us prompt tokens, completion tokens, and total tokens per agent, per subscription, queryable in Application Insights. When someone asks what the weather agent cost last week, the answer is a query, not an estimate.

The semantic caching policy sits in front of the model and short-circuits repeat questions:

xml

<azure-openai-semantic-cache-lookup
score-threshold="0.85"
embeddings-backend-id="embeddings-backend"
embeddings-backend-auth="system-assigned">
<vary-by>@(context.Request.Headers.GetValueOrDefault("X-Agent-Id", "unknown"))</vary-by>
</azure-openai-semantic-cache-lookup>

A cache hit costs an embedding call instead of a full completion. For an agent that answers weather questions, where “what’s the weather in Amsterdam” arrives in twenty phrasings, that’s not a rounding error.

In FinOps for AI terms, the first policy lives in the Understand Usage and Cost domain, and the second in Optimize Usage and Cost. So far, the framework and the implementation agree.

Where the mapping holds up

Two places, and one of them matters more than I expected before reading the article.

Semantic caching is a named lever. The FinOps article lists it explicitly as an engineering-side optimization, and the Citadel implementation has it running in production policy XML, not on a roadmap slide. Score threshold tuning is real work (0.85 took iterations, and I documented the false-positive risk in the original policy deep dive), but the lever exists, and it’s been pulled.

The gateway is the anti-aggregator, and the FinOps article’s sharpest warning is that token consumption is being hidden in SaaS subscriptions, where a flat monthly price hides a metered reality beneath the surface. The hub-and-spoke model is the architectural inverse of that problem. Nothing reaches a model without crossing the gateway, so nothing consumes tokens invisibly. The whole point of Part 2 in the Citadel series was to refuse the path that bypasses the meter when the Agent Service SDK tries to call the model directly.

I’d go one step further than the FinOps article does. Centralized metering isn’t just a FinOps convenience. It’s the same choke point that enforces content safety and the kill switch. Cost visibility and governance aren’t two systems in this architecture, they’re one policy pipeline.

Where Citadel’s token economics fall short

This is the useful part, because the gaps are specific.

  • No goodput tracking: The Hub knows how many tokens the agent consumed. It does not know how many of them were worth consuming. Time-to-first-token and tokens-per-second aren’t captured as dimensions, and nothing distinguishes a completion the user acted on from one that got regenerated three times. By the article’s standard, Citadel measures throughput and calls it a day.
  • No token yield rate: Closely related, but distinct. Yield asks: cost per successful outcome, not per call. The weather agent writes every conversation to Cosmos DB (Part 3 of the series), so the raw material for outcome tagging exists. Nothing joins it to the token metrics yet. That’s a gap in instrumentation, not in data.
  • No model routing: Every query hits the same deployment, whether it’s “weather in Ede” or a multi-step tool-calling chain. The article’s Pareto framing (bulk tokens, mid-tier tokens, premium low-latency tokens, reasoning tokens, drawn from SemiAnalysis’s InferenceX benchmarking) implies a cascade: cheap model first, escalate on need. APIM can express this with backend pools and routing policy. Citadel doesn’t, yet.
  • Tool-schema overhead is unmeasured: Every tool-calling request carries the Open-Meteo tool definition in the payload, on every single call. One tool, so the overhead is small. But the Cloudflare finding the article cites is a warning about what happens at ten or twenty tools, and I have no metric today that would even show me the problem growing.

Why this bites harder on agentic workloads

There’s a compounding effect the article touches on that I can back with a documented example. Orchestration overhead isn’t a fixed tax, it multiplies through agent chains.

In the Logic Apps Agent Loop series, I found that sequential agents don’t pass plain strings between each other. Each agent action returns a structured JSON messages array, and you need a Compose action to bridge it into the next agent. Every one of those bridged payloads is tokens. Single-agent token math is linear. Multi-agent token math is not, and that’s where token economics stops being a dashboard exercise. If your metering only captures totals per call, the orchestration overhead hides inside numbers that look individually reasonable.

Diagram comparing expected linear token cost of a three-agent chain against actual cost. The top row shows three agents each assumed to cost one unit. The bottom row shows each agent's payload growing as it carries the previous agents' messages arrays across Compose bridges, so the chain costs well over three units.
Per-call totals look reasonable in isolation. Each chained agent drags the accumulated context of every agent before it.

What I’d add to the Citadel Hub next

In order of effort against payoff:

  • Outcome tagging first: The conversations container already holds every run. Adding a resolution field (answered, retried, abandoned) and joining it against the token metrics in Application Insights gets me a real token yield rate with no new infrastructure. This is the cheapest gap to close and the one that changes the conversation from “what did we spend” to “what did we get.”
  • Latency dimensions second: Emitting time-to-first-token and total duration alongside the existing token dimensions turns the same App Insights workspace into a goodput dashboard. APIM sees the timing already, it just doesn’t emit it.
  • A routing experiment third: The weather agent is a good candidate for a two-tier cascade precisely because it’s boring. Simple lookups go to a small model, tool-calling chains escalate. If the cascade breaks the agent, it breaks it cheaply, and I’ll write up whatever goes wrong.

Tool-schema compression stays on the watch list rather than the to-do list. With one tool, measuring it first beats optimizing it blind.

Pitfalls

Adopting the vocabulary without the substance is the most common trap. It’s easy to say ‘we do token economics’ because a dashboard shows token counts. Raw volume without yield or goodput is accounting, not economics. The article’s framework is only useful if the uncomfortable metrics come with it.

Treating flat-price AI tools as flat costs is the second trap. When teams around you adopt AI SaaS tooling, those subscriptions consume tokens on someone’s meter. Budgeting them as fixed line items repeats the exact mistake the article warns about, one procurement layer up.

Optimizing the cache before understanding the traffic is the last one. A semantic cache with an aggressive threshold saves tokens and quietly serves wrong answers. Tune against logged real queries, never against the token savings number alone. I learned this at 0.85, and the number that’s right for a weather agent is wrong for an agent where two similar-sounding questions need different answers.

Closing

The FinOps article gives this space the vocabulary it needs, and the Citadel Platform gives me somewhere to test that vocabulary against running policy XML. The scorecard: attribution and caching, solid. Goodput, yield, and routing: real gaps with concrete next steps.

The bigger takeaway is architectural. Every improvement on that list lands in the same place, the gateway. APIM started this series as a governance layer. It’s ending it as the FinOps instrumentation layer too, and I don’t think that’s a coincidence. The choke point that can say no to a request is the same choke point that can tell you what the request cost.

If you’re metering your own agent platform, I’d like to hear which of these gaps you closed first, and whether the yield numbers surprised you.

Hosting MCP Servers on Azure Functions: GA vs. Preview

The Model Context Protocol (MCP) has become the standard way AI agents and models interact with external systems. If you are building something on Azure and need to expose tools to an AI client, the question is no longer whether to use MCP; it is which hosting option to choose.

Azure Functions supports three distinct approaches. Two of them host MCP servers directly. The third uses message queues instead of MCP calls altogether. Each reflects a different set of trade-offs. This post maps them out so you can pick the right one before you write any code.

Why use Azure Functions for hosting MCP servers?

The doc makes the case concisely: Functions scales efficiently to handle demand and provides binding extensions that simplify AI integration. Both matter for MCP hosting specifically.

MCP servers are called on demand. An AI client sends a request, the server responds, and then goes quiet. That is a bursty, event-driven pattern. Flex Consumption and Elastic Premium both handle it well. Flex Consumption gives you scale-to-zero billing for bursty tool workloads. Elastic Premium gives you pre-warmed instances, predictable latency, and VNet integration for tools that need to reach internal APIs and databases.

Managed identity is the other key piece. Your MCP server likely needs to call downstream Azure services: blob storage, Cosmos DB, and Azure AI Search. With managed identity, you do not need to manage credentials inside your function code, and you do not need to pass secrets through your MCP configuration.

The three ways to host MCP servers on Azure Functions

Comparison matrix with three columns โ€” MCP binding extension (teal, GA), self-hosted MCP SDK (amber, preview), and queue-based tool (coral, GA) and six feature rows: support level, programming model, stateful execution, transport requirement, implementation mechanism, and best-fit scenario.
The three Azure Functions MCP hosting options compared across six features. Amber cells flag preview constraints.

Option 1: MCP binding extension (GA)

This is the right default for most teams. The binding extension lets you build an MCP server using the standard Functions programming model triggers, bindings, local development with Core Tools, and deployment via azd. You annotate a function with McpToolTrigger, and it is automatically exposed as an MCP tool.

The comparison table from the Microsoft Learn documentation makes the feature set clear:

FeatureMCP binding extension
Support levelGA
Programming modelFunctions, triggers and bindings
Stateful executionSupported
LanguagesC# (isolated), Python, TypeScript, JavaScript, Java
Other requirementsNone
ImplementationMCP binding extension

Stateful execution support is the binding extension’s meaningful advantage over the self-hosted SDK option. If your MCP server needs to maintain session state across tool calls, for example, a multi-turn data retrieval scenario, only the binding extension handles that today.

The quickstart template (remote-mcp-functions-dotnet) is a useful starting point. It scaffolds the project via azd init, runs locally with the Azurite storage emulator, and includes a .vscode/mcp.json file that wires the local endpoint directly into GitHub Copilot’s agent mode for testing. That local-to-Copilot loop is genuinely quick; you can test a tool call from Copilot chat without deploying anything.

Before you build: a toolchain issue to know about

There is a confirmed bug in Microsoft.Azure.Functions.Worker.Sdk (all versions to 2.0.7) where the auto-generated WorkerExtensions.csproj is hardcoded to net6.0. The MCP extension package requires net8.0, so the build fails out of the box with NU1202: Package is not compatible with net6.0. The Microsoft Learn quickstart does not mention this.

The workaround that actually works has three parts: add an extensionBundle to host.json so the host loads MCP support at runtime rather than through the build-time generator; mark all extension PackageReference entries with PrivateAssets="all" so their build targets don’t re-trigger the generator; and use 1.0.0-preview.3 rather than 1.0.0 or later preview versions, because the generated project still targets net6.0 and needs a package version it can restore against that framework. The companion repo has all three changes applied and a detailed explanation in the README.

The McpToolTrigger attribute is the core mechanism. Here is what the minimal C# version looks like from the quickstart:

[Function(nameof(SayHello))]
public string SayHello(
[McpToolTrigger(HelloToolName, HelloToolDescription)] ToolInvocationContext context
)
{
logger.LogInformation("C# MCP tool trigger function processed a request.");
return "Hello I am MCP Tool!";
}

The trigger attribute registers the function as an MCP tool and handles the protocol negotiation. You write the tool logic; the extension handles the MCP wire format.

Use this when: you are building a new MCP server and do not have an existing codebase using MCP SDKs. It is the fastest path from zero to a deployed, governed MCP server on Azure.

Avoid it when: you already have an MCP server built with an official MCP SDK and want to lift it into Azure without rewriting it in the Functions model.


Note: Microsoft has samples for this option. You can find all the MCP extension samples atย aka.ms/remote-mcp

Option 2: Self-hosted MCP servers via official MCP SDKs (preview)

This option lets you take an existing MCP server built with the official MCP SDKs and host it on Azure Functions without rewriting it as a binding-extension-style function app. Functions acts as the hosting runtime; your MCP SDK code runs via custom handlers.

FeatureSelf-hosted MCP servers
Support levelPreview
Programming modelStandard MCP SDKs
Stateful executionNot currently supported
LanguagesC# (isolated), Python, TypeScript, JavaScript, Java
Other requirementsStreamable HTTP transport
ImplementationCustom handlers

Two constraints define whether this option is viable for you right now.

  • First: Streamable HTTP transport is required. The self-hosted option does not support Server-Sent Events (SSE) transport. Suppose your existing MCP server relies on SSE, which many early implementations did; you need to migrate to Streamable HTTP before hosting on Functions. That is a non-trivial change for an established codebase.
  • Second: stateful execution is not supported during preview. If your MCP server is stateless most tool servers are this is not a problem. But if you need session continuity across calls, stay with the binding extension.

The documentation also flags that configuration details for self-hosted MCP servers change during the preview period. That means operational overhead: you may need to adjust configuration as the feature evolves. Factor that into a production timeline.

The sample (Weather server) shows the self-hosted pattern working correctly for a stateless tool. It is a useful reference for understanding the custom handler wiring, but it does not represent a feature-complete production deployment yet.

Use this when: you have an existing MCP server built with official MCP SDKs, it uses Streamable HTTP transport, it is stateless, and you want to run it on Functions without adopting the binding extension model.

Avoid it when: you need stateful execution, you rely on SSE transport, or you are building from scratch. In those cases, the binding extension is the better path.

Note: There is another approach for hosting official MCP SDK-based servers. This approach is about hosting MCP in Connector Namespace, which was announced in public preview at Build.


Option 3: Queue-based Azure Functions tools

This is the option that gets overlooked because it does not follow the MCP pattern at all, and that is sometimes exactly what you want.

Instead of an AI agent calling your function via the MCP protocol, the agent sends a message to a queue (Service Bus or Azure Storage Queue), and a queue-triggered function picks it up asynchronously. Foundry provides Azure Functions-specific tooling for this pattern.

The Microsoft Learn documentation lists the scenarios where this is the right choice:

  • Reliable message delivery and processing
  • Decoupling between AI agents and function execution
  • Built-in retry and error handling
  • Integration with existing Azure messaging infrastructure

That last point is often what decides it in enterprise contexts. If your integration platform already uses Service Bus for order processing, claims workflows, or event-driven pipelines, the queue-based tool pattern slots into that infrastructure without adding a new protocol layer. Your AI agent becomes another message producer; your function is another consumer. Operations teams already know how to monitor and manage it.

The decoupling is also genuinely useful for long-running tool operations. An MCP call is synchronous from the agent’s perspective: it sends a request and waits for a response. A queue-based call lets the agent fire and move on; the function processes in the background and the result arrives via a separate callback or polling mechanism. For operations that take seconds or minutes document processing, batch retrieval, report generation that asymmetry matters.

Use this when: the tool operation is long-running, you need reliable delivery with built-in retry, you are integrating with existing Azure messaging infrastructure, or you want to decouple the agent from function execution timing.

Avoid it when: the agent needs a synchronous response immediately, or you are connecting to AI clients that expect the MCP protocol specifically.

Note: For long-running processes or multi-step workflows, you might also want to consider Durable Functions instead of “vanilla” Functions. Durable Functions provides built-in state persistence and automatic retries, which is helpful when long-running or multi-step workflows fail mid-execution because of things like unreliable network connectivity. Because state is persisted during execution, Durable Functions can rebuild local state up to the point of failure and continue executing from there instead of from scratch.ย 


How to choose

SituationOption
Building a new MCP server from scratchBinding extension
Need stateful tool executionBinding extension
Have an existing SDK-based MCP server (Streamable HTTP, stateless)Self-hosted SDK
Need async, decoupled, fault-tolerant tool executionQueue-based
Integrating with existing Service Bus infrastructureQueue-based
Building for GitHub Copilot or VS Code agent modeBinding extension
Production deadline in the next quarterBinding extension or queue-based (avoid preview)
I need pre-warmed instances and predictable latencyElastic Premium plan

The preview constraint is the most important practical point here. If you are building for production and your timeline does not allow for configuration changes mid-project, self-hosted MCP servers are not ready. The binding extension and queue-based options are both GA and stable.

APIM as the governance layer

Whichever option you choose, consider putting APIM in front of your MCP server endpoint. This gives you rate limiting, authentication policy, token quota management, and a single point for logging and monitoring across all MCP tool calls.

One practical caveat from real deployment experience: the Azure AI Foundry Agent Service SDK routes LLM calls directly to Azure OpenAI, bypassing APIM entirely. Tool calls to your MCP server do pass through APIM, but model calls do not unless you use the standard OpenAI SDK instead. The Citadel Platform series covers this in detail, including how to structure your APIM policies to handle the tool-call traffic that does flow through it.

What comes next

The next post goes deeper into the serverless agents runtime, the preview programming model that lets you define event-triggered agents as function apps, with .agent.md files, agents.config.yaml, and remote MCP server connections declared in mcp.json.

Up next: The Azure Functions Serverless Agents Runtime: What It Is and When to Use It

Registering a Citadel Agent in the AI Registry: Azure API Center

This is the final post in a five-part series on the Microsoft Foundry Citadel Platform about Azure API Center AI agent registration. If you landed here first, a quick catch-up helps.

In Part 1, I deployed the Citadel Governance Hub (APIM, Azure OpenAI, Content Safety, Cosmos DB, Logic App) and an Agent Spoke (AI Foundry, Cosmos DB, Key Vault, App Config) in Sweden Central. Next in Part 2, I connected a tool-calling agent through APIM using the standard OpenAI SDK, with the Open-Meteo weather API as its only tool. In Part 3, the agent started writing every run to a conversations container in the spoke’s Cosmos DB. In Part 4, I built a three-layer kill switch so the platform team could shut an agent down fast when something went wrong.

By the end of Part 4, I had a working agent with a gateway, an audit trail, and an off switch. But I still had a gap that only shows up once you stop thinking about one agent and start thinking about a fleet of them: nobody outside my own head knew this agent existed.

That’s the problem this post solves.

Why register agents at all

One agent is easy to track. You know its name, its resource group, and roughly what it does, because you built it last week. Ten agents get harder. A hundred agents, spread across teams, spokes, and environments, becomes a governance problem rather than an inconvenience.

Platform teams eventually ask the same three questions about every agent in the estate: what does it do, what data does it touch, and who owns it. Without a registry, the answer lives in Slack threads, README files, and the memory of whoever built the thing. That doesn’t scale, and it definitely doesn’t survive an audit.

Azure API Center gives the Citadel Platform a place to answer those questions consistently, for every agent, in a format a platform team (or a compliance officer) can actually query. And as you’ll see later in this post, that same discipline lines up neatly with a transparency obligation that’s no longer theoretical: Article 49 of the EU AI Act.

Azure API Center 101

API Center and Azure API Management solve different problems, even though they sound similar and live in the same governance hub.

APIM is a runtime gateway. It sits in the request path, enforces policies, and does the actual work of routing, throttling, and inspecting traffic. That’s where the kill switch from Part 4 lives, and it’s where the five governance policies from earlier in the series get enforced.

API Center is a catalog. It doesn’t sit in the request path at all. Its job is to hold structured metadata about APIs and, in our case, agents: what they are, what version they’re on, who owns them, and what environment they run in. Think of APIM as the checkpoint and API Center as the registry office.

In the hub resource group rg-ai-hub-gateway-dev, the API Center instance is already deployed as apic-wpvlimv4ngkns, alongside APIM. That placement matters. The registry belongs in the hub because it needs to see across every spoke, not just the one running our weather agent.

The AI Publish Contract pattern

A generic API definition (an OpenAPI spec, for example) tells you the shape of the requests and responses. It doesn’t tell you whether the thing behind that shape is calling a third-party model, touching personal data, or making decisions that affect a person’s rights.

For agents, that gap matters more than it does for a plain REST API. So the Citadel Platform defines a small, structured metadata contract that every agent has to carry before it gets registered. I call it the AI Publish Contract. It rides alongside the API definition in API Center as custom metadata, rather than replacing the API definition entirely.

Here’s what it looks like for the weather agent:

json

{
"contractVersion": "1.0",
"agent": {
"id": "citadel-weather-agent-v1",
"displayName": "Citadel Weather Agent",
"description": "Tool-calling agent that answers weather queries using Open-Meteo as its only external tool.",
"owner": "platform-team@example.com",
"environment": "dev",
"spokeResourceGroup": "rg-ai-spoke-dev",
"aiFoundryProject": "aifp-tggi2gmkw22w4"
},
"modelBacking": {
"provider": "azure-openai",
"routedThrough": "apim-wpvlimv4ngkns",
"model": "gpt-4o-mini"
},
"tools": [
{
"name": "open-meteo-forecast",
"type": "external-api",
"endpoint": "https://api.open-meteo.com/v1/forecast",
"dataClassification": "public"
}
],
"dataClassification": "public",
"personalDataProcessed": false,
"highRiskCategory": false,
"killSwitchLayer": "named-value-flip",
"transparencyDisclosure": {
"aiActArticle49Applicable": false,
"userFacingDisclosureRequired": true,
"disclosureText": "This response was generated by an AI system."
}
}

A few fields deserve a comment, because I went back and forth on them.

dataClassification and personalDataProcessed exist because a registry that doesn’t record data sensitivity is only half a registry. The weather agent is a deliberately boring example: no personal data, no high-risk category, public weather data in and out. That’s exactly why it’s a good agent to demonstrate the pattern on before you register something that touches real user data.

transparencyDisclosure maps directly to the Article 49 discussion later in this post. Even for a low-risk agent, I keep the field in the contract rather than making it conditional. Consistency across every agent in the registry is the whole point.

Step-by-step: registering the weather agent in Azure API Center

With the contract defined, registration itself is a handful of steps. I’ll show the CLI first, then the portal equivalent, because I use both depending on whether I’m scripting a pipeline or walking a colleague through it live.

Prerequisites

You need an existing API Center instance (ours is apic-wpvlimv4ngkns in rg-ai-hub-gateway-dev) and Contributor access to it. If you’re following along, confirm the instance is visible first:

az apic show --resource-group rg-ai-hub-gateway-dev \
--name apic-wpvlimv4ngkns \
--subscription dc0f4d72-3734-4b03-8884-ccfb9c2c4cc7

Step 1: define the custom metadata schema

API Center lets you extend its metadata model with custom fields. Before registering any agent, define a schema for the AI Publish Contract so every future registration validates against the same structure.

az apic metadata create --resource-group rg-ai-hub-gateway-dev \
--service-name apic-wpvlimv4ngkns \
--metadata-name aiPublishContract \
--schema "{\"type\":\"string\",\"title\":\"AI Publish Contract\"}"
--assignments "[{\"entity\":\"api\",\"required\":true}]"

I keep ai-publish-contract-schema.json as a JSON Schema file that mirrors the structure shown above. Doing this once means every agent registered afterward gets validated the same way, instead of drifting field by field as different teams register their own agents.

Step 2: register the agent as an API

az apic api create --resource-group rg-ai-hub-gateway-dev \
--service-name apic-wpvlimv4ngkns \
--api-id citadel-weather-agent-v1 \
--title "Citadel Weather Agent" \
--type rest \
--custom-properties @C:\Users\steef\ai-citadel-agent\citadel-agent\ai-publish-contract.json

The --type rest flag looks like a mismatch at first, since this is an agent, not a REST API in the traditional sense. API Center doesn’t yet have a native “agent” type, so I register it as a REST API and let the AI Publish Contract metadata carry the agent-specific detail. That’s a workaround, not a limitation I’d defend as elegant, and I expect this to get cleaner as Microsoft extends API Center for agentic workloads.

Step 3: create the API version

az apic api version create --resource-group rg-ai-hub-gateway-dev \
--service-name apic-wpvlimv4ngkns \
--api-id citadel-weather-agent-v1 \
--version-id v1-0 \
--title "v1" \
--lifecycle-stage development

Step 4: attach the definition

az apic api definition create --resource-group rg-ai-hub-gateway-dev \
--service-name apic-wpvlimv4ngkns \
--api-id citadel-weather-agent-v1 \
--version-id v1-0 \
--definition-id openapi \
--title "OpenAPI"

Then import the actual spec, which describes the agent’s HTTP-facing invocation contract (the endpoint APIM exposes, not the internal tool-calling logic):

az apic api definition import-specification --resource-group rg-ai-hub-gateway-dev \
--service-name apic-wpvlimv4ngkns \
--api-id citadel-weather-agent-v1 \
--version-id v1-0 \
--definition-id openapi \
--format link
--value "https://apim-wpvlimv4ngkns.azure-api.net/weather-agent/openapi.json"
--specification "{\"name\":\"openapi\",\"version\":\"3.0.0\"}"

That failed immediately:

(ValidationError) Could not download the API specification from
the specified URL due to an error:
Could not reach the provided URL.
Please make sure that the specification file
is publicly available and that the link is valid and up-to-date.

My first assumption was a network or auth issue between API Center and APIM. It wasn’t. Curling the same URL directly returned a plain 404 from APIM itself:

{ "statusCode": 404, "message": "Resource not found" }

That 404 is the real signal. There is nothing published at that path, because there was never a dedicated API resource for the weather agent in APIM to begin with. Part 2 wired the agent up through APIM as a passthrough to Azure OpenAI, using the standard OpenAI SDK. That gets you routing, policy enforcement, and governance on every call, but it doesn’t give you a discoverable, documented API surface. A passthrough proxies requests. It doesn’t publish an OpenAPI spec describing them.

This is worth sitting with for a second, because it’s not just a command-line gotcha. It means the OpenAPI definition API Center wants isn’t something you can export from a running agent. You have to author it, the same way you’d write documentation, because the system underneath genuinely has nothing to hand you.

So the real fix has two parts. First, write a minimal OpenAPI spec that documents the agent’s actual invocation contract as it exists through the passthrough:

{
"openapi": "3.0.0",
"info": {
"title": "Citadel Weather Agent",
"version": "1.0.0",
"description": "Tool-calling agent invocation contract, routed through APIM to Azure OpenAI."
},
"servers": [
{ "url": "https://apim-wpvlimv4ngkns.azure-api.net" }
],
"paths": {
"/openai/deployments/{deploymentId}/chat/completions": {
"post": {
"summary": "Invoke the weather agent",
"parameters": [
{
"name": "deploymentId",
"in": "path",
"required": true,
"schema": { "type": "string" }
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"messages": { "type": "array" },
"tools": { "type": "array" }
},
"required": ["messages"]
}
}
}
},
"responses": {
"200": {
"description": "Chat completion response, including any tool calls the agent made.",
"content": {
"application/json": {
"schema": { "type": "object" }
}
}
}
}
}
}
}
}

Save that locally as openapi.json, then import it inline instead of by link, since there’s nothing at the other end of a link to point to:

az apic api definition import-specification --resource-group rg-ai-hub-gateway-dev \
--service-name apic-wpvlimv4ngkns \
--api-id citadel-weather-agent-v1 \
--version-id v1-0 \
--definition-id openapi \
--format inline \
--value @C:\Users\steef\ai-citadel-agent\citadel-agent\openapi.json \
--specification "{\"name\":\"openapi\",\"version\":\"3.0.0\"}

That worked. But notice what actually happened here. The spec isn’t extracted from the system, it’s a description you write and commit to keeping accurate. If you add a second tool to the agent, or change the invocation shape, this file goes stale unless you treat it as part of the change, not an afterthought to registration.

Step 5: register the environment and deployment

az apic environment create \
--resource-group rg-ai-hub-gateway-dev \
--service-name apic-wpvlimv4ngkns \
--environment-id dev \
--title "Development" \
--type development
az apic api deployment create --resource-group rg-ai-hub-gateway-dev \
--service-name apic-wpvlimv4ngkns \
--api-id citadel-weather-agent-v1 \
--deployment-id dev-deployment \
--title "Dev deployment" \
--environment-id "/workspaces/default/environments/dev" \
--definition-id "/workspaces/default/apis/citadel-weather-agent-v1/versions/v1-0/definitions/openapi" \
--server "{\"runtimeUri\":[\"https://apim-wpvlimv4ngkns.azure-api.net/weather-agent\"]}"

The portal equivalent FOR Azure API Center

If you’d rather click through it, the same five steps map onto the portal like this. Open the API Center resource in the Azure portal, and go to APIs. Select “Register API,” fill in the title and type, and paste the AI Publish Contract JSON into the custom metadata panel that appears once the metadata schema is assigned. From there, add a version, upload or link the OpenAPI definition, and finish by adding an environment and a deployment under the API’s Deployments tab.

I default to the CLI for anything I’ll repeat across agents, and the portal when I’m registering something once or explaining the process to someone new to the platform.

What it looks like in the fleet dashboard

Once registration finishes, the weather agent shows up in the Azure API Center catalog alongside anything else registered in the hub. The catalog view lists each API (agent) with its title, type, and lifecycle stage, and clicking into it surfaces the custom metadata panel with the full AI Publish Contract attached.

This is where the payoff becomes visible. Instead of a spreadsheet someone updates twice a year, the platform team gets a live, filterable view. Filter by dataClassification: personal and you instantly see which agents touch sensitive data. In addition, filter by environment: production and you get the actual production fleet, not the fleet someone remembers deploying. And finally, filter by owner, and you know exactly who to page when an agent misbehaves.

For a single weather agent, this looks like overkill. It isn’t the weather agent that justifies the pattern. It’s the fortieth agent, registered by a team you’ve never met, that you’ll be glad has the same contract structure as this one.

Connecting to EU AI Act Article 49

Article 49 of the EU AI Act introduces a registration obligation, primarily for providers and deployers of high-risk AI systems, who must register certain information in an EU database before those systems go into service or are used. The exact scope depends on the system’s risk classification, and I’m not going to pretend to give legal advice here. Talk to your compliance team about whether a specific system falls under that obligation.

What I can talk about is the architecture. The AI Publish Contract pattern doesn’t make an agent compliant with Article 49. What it does is put your organization in a position to answer an Article 49-style question quickly: which AI systems exist, what do they do, what data do they process, and are any of them high-risk. That’s the same question a platform team asks internally for entirely operational reasons, and it turns out to be the same question a regulator asks for entirely different reasons.

Building the registry now, before any single agent forces the issue, means the answer already exists when someone asks. Retrofitting a registry across dozens of agents that were never designed with one in mind is a much worse project to inherit.

The weather agent in this post sets aiActArticle49Applicable: false, because it’s a low-risk, non-personal-data agent. The field exists precisely so the next agent, the one that does touch personal data or make consequential decisions, has a place to say true and trigger whatever process your organization builds around that flag.

Pitfalls

A few things caught me off guard while working through this. I’m flagging these as the kind of issues I’d expect to hit on a real rollout, so treat them as a checklist to verify against your own environment rather than a transcript of my exact errors.

  • A passthrough agent has no OpenAPI spec to export, even though it looks like it should. This is the one that actually got me. Importing by link failed with a vague reachability error from az apic, which pointed me toward a networking explanation. Curling the URL directly gave the real answer: a plain 404 Resource not found from APIM. There was nothing published at that path, because Part 2 wired the agent up as a passthrough to Azure OpenAI, not as a dedicated API resource with its own documented surface. Passthroughs proxy requests, they don’t publish specs. The fix is to hand-author a minimal OpenAPI spec describing the invocation contract and import it inline, and then treat that file as something you update deliberately when the agent’s interface changes, not something the platform keeps in sync for you.
  • Custom metadata schema assigned after APIs already exist. If you register an API before creating the metadata schema, the custom properties won’t validate retroactively. Define the schema first, every time, or you’ll end up patching APIs one by one afterward.
  • RBAC gaps across the hub-spoke boundary. The identity registering the agent needs Contributor (or a custom role with equivalent permissions) on the API Center instance in the hub, which is a different resource group than where the agent itself runs. Teams that only have access to their own spoke will hit a permissions wall here, and that’s worth deciding on deliberately rather than discovering it during a rollout.

Other Pitfalls

  • Confusing API Center registration with APIM configuration. These are separate systems that happen to sit in the same hub. Registering an agent in API Center doesn’t change how APIM routes or governs it. I’ve seen (and made) the assumption that registration alone would trigger some policy change in APIM. It doesn’t. They’re linked by convention and shared metadata, not by any automatic sync.
  • Definition drift. The OpenAPI definition describes the HTTP surface of the agent, but the AI Publish Contract describes its behavior and data handling. Nothing enforces that these stay in sync when the agent changes. Treat both as part of your deployment pipeline, updated together, rather than the contract as a one-time registration artifact.
  • Lifecycle stage mismatches. Marking an API’s --lifecycle-stage as development when it’s actually running in a shared dev/test environment used by multiple teams creates confusion during the fleet review. Match lifecycle stages to how the environment is actually used, not just what resource group it happens to sit in.

What’s next

This closes out the deployment arc of the series: hub and spoke, a working agent, persistence, a kill switch, governance policies, and now a registry entry. What I haven’t covered yet, and what comes next, is hardening this for anything beyond a single dev environment. That means promotion paths from dev to staging to production, cost controls that scale with a growing agent fleet, and staging environment parity so what you test actually matches what you ship.

If there’s interest, that hardening work is a natural follow-up series rather than a single post, given how much ground it covers.

Wrapping up the series

Five posts ago, this started as an empty resource group in Sweden Central. It’s now a governed platform: a hub that enforces policy and holds the registry, a spoke that runs the agent, a kill switch that can shut things down fast, and a contract-based registration pattern that keeps the fleet auditable as it grows.

The weather agent was never really the point. It was the simplest possible agent I could use to prove out every layer of the platform without the complexity of a real production workload getting in the way. The patterns here, hub-spoke isolation, layered kill switches, policy-driven governance, and contract-based registration, are the parts meant to outlast this specific agent.

Thanks for following along through the series. If you’re building something similar, I’d like to hear what broke for you.

Logic Apps Automation Preview

Microsoft announced Azure Logic Apps Automation at Build 2026 and put it straight into public preview at auto.azure.com. The launch framing was “automation just became a team sport“. The product framing is a managed SaaS experience: you sign in, and compute, connectors, model endpoints, and knowledge services are already there.

I have spent the last months in the Logic Apps agent loop on Standard, and I wrote a seven-part series about what actually breaks there. So my first reaction to Automation was not “new designer, nice”. It was a governance question: who owns the workflow, who can read it, and what happens when the person who built it leaves?

That question turns out to be the most interesting thing about this release, and almost nobody is writing about it.

This post covers what Automation is, how it works, a scenario you can build and demo in about half an hour, and an honest list of what preview does not do yet.

Logic Apps Automation: What it actually is

Strip the marketing and Automation is four things at once:

  • A new SKU on the same engine. The Logic Apps runtime is unchanged. The connector catalogue, 1,400 plus, is the same one you already use. Expressions, control flow, stateful and stateless workflows, draft and published versions: all familiar. Adam Marczak put it well after testing it: Automation is new, but agentic Logic Apps is not.
  • A new hosting model. Consumption is multitenant and scales to zero. Standard is single tenant but you provision and pay for capacity. Automation is single tenant with a dedicated runtime boundary, Microsoft manages the hosting capacity, and it scales 0 to N. That combination did not exist before.
  • A new resource hierarchy. Project, then Application, then Workflow. Sandboxes sit at project level. This is the part that changes how you govern.
  • A new portal. auto.azure.com is not a replacement for the Azure portal. It sits alongside it. Projects show up as Azure resources in your resource group, but you build in the new experience.
Diagram of the Logic Apps Automation resource hierarchy: a Project contains an Application, which contains Workflows with draft and published versions, alongside project-level Sandboxes and Project settings. A second column shows the two permission scopes, project and app, plus Owner as a property and the boundary where project admins see app metadata only.
Figure 1. Project, Application, Workflow, plus the two permission scopes that sit on top of them. Project admins see app metadata for governance, not workflow contents, connections, or run history.

Logic Apps Sandbox

One capability worth pausing is the Logic Apps Sandbox: built-in Python code execution within the agent loop, running in a secure, isolated environment with no external compute resource required. In the Standard agent loop series, I covered tools as connector actions the model can invoke at runtime. Sandbox adds a different category entirely: the agent writes code, executes it, observes the output, and iterates. Data transformation, chart generation, CSV processing, and dynamic API calls tasks that connectors alone cannot handle and that previously required an Azure Function or a custom action sitting outside the workflow. In Automation, Sandbox is already provisioned alongside the model endpoints and connectors. You do not configure it. You enable the Code Interpreter toggle in the agent action, and the capability is there. That is the managed SaaS promise made concrete, and it is the scenario I plan to cover in a dedicated follow-up post.

Where it fits next to the SKUs you already run

Positioning is now clearer than it was in the first 48 hours after Build. Power Automate is personal and team productivity inside Microsoft 365. Logic Apps are the enterprise integration platform: SAP, EDI, B2B, high-throughput API orchestration, predictable 24×7 load. Consumption remains excellent for sporadic, event-driven work.

Automation aims at the gap in the middle. Teams that need enterprise grade infrastructure but want a SaaS experience and AI assisted creation.

Bubble chart plotting four Microsoft workflow products by builder profile, from business team to integration developer, against infrastructure ownership, from fully managed to customer provisioned. Power Automate and Logic Apps Consumption sit in the managed lower area, Logic Apps Standard in the customer-provisioned upper right, and Logic Apps Automation is highlighted in the middle as single-tenant isolation with Microsoft-managed capacity.
Figure 2. Automation is not a replacement for Standard. It fills the gap between a productivity tool and an integration platform: single-tenant isolation, but Microsoft manages the capacity.

A short decision table, and I am reading direction here rather than quoting a Microsoft matrix:

ScenarioWhere I would put it today
SAP, EDI, B2B, high throughput API orchestrationStandard
Central integration platform, predictable 24×7 loadStandard
Simple event driven automation, low volumeConsumption
AI and agent workloads with bursty trafficAutomation
Long idle periods with traffic spikesAutomation
Departmental workflow owned by a business teamAutomation, with the caveats in section 5
Personal productivity inside Microsoft 365Power Automate

How an agentic workflow actually runs

An agent in Automation is a workflow action backed by a model. You give it a system prompt, a toolset, an input, and downstream actions consume its output. There are two flavours.

Native agents run the loop inside the workflow runtime. Every iteration appears in the execution log. Tools are the action nodes you place inside the agent boundary, plus a Code Interpreter toggle.

Foundry agents hand off to Azure AI Foundry Agent Service. The workflow sees one call and a final output. You pick this when the assistant already exists in Foundry.

The switch between them is a config change in the AI model dropdown. The rest of the workflow does not care. That is a genuinely good design decision.

Diagram of a native agent action at runtime. A trigger feeds an agent boundary in which the model loops with its tools, issuing tool calls and receiving tool results, grounded by knowledge bases and a sandbox. The final structured output flows to deterministic downstream actions. An observability lane below shows that every step writes to real-time run history.
Figure 3. The loop, the tools, the grounding, and the lane where you debug all of it. Branch on the agent’s structured output, not on its prose answer.

Two expression details worth memorising, because they are the seams where things break:

@outputs('AgentName')['lastAssistantMessage']
@outputs('AgentName')['structuredOutput']?['severity']

and inside a tool, to read what the model decided to pass you:

@agentParameters('errorCode')

Build on structuredOutput, not on the final message. In my blog series, I hit this the hard way: agent output arrives as a structured payload, and reaching for the prose answer produces workflows that pass a demo and fail on the third real message.

Logic Apps Automation: A scenario you can build and demo

Here is a demo that survives contact with an audience. It uses an HTTP trigger, which matters: HTTP and manual triggers fire from a draft, so you can iterate with Test your draft without publishing. Schedule and event driven triggers only fire against the published workflow.

The scenario: integration failure triage. A message lands on a dead letter queue. Instead of paging a human with a JSON blob, an agent classifies the failure, checks the runbooks, decides whether it is retryable, and either posts a structured summary to the on call channel or raises a ticket. The deterministic parts stay deterministic.

Flow diagram of the integration failure triage demo. An HTTP trigger receives a dead-letter payload, Parse JSON types it, and a triage agent with lookup, recent-failures, code-interpreter, and runbook-knowledge tools returns a structured verdict. Three deterministic branches follow: high severity raises a ticket, retryable requeues, and everything else goes to a digest.
Figure 4. A demo that shows the loop, the grounding, and the deterministic guardrails around both. The convincing run is the one where the agent declines to invent an answer.

CREATE Project

  • Create a project at auto.azure.com, then create an application inside it. Wait for the status to flip from Building to Ready before you open it. This will take some time.
  • Start the workflow. You can type the intent into the assistant box, or click Build from scratch. For a demo I build from scratch, because every step stays visible and explainable.
  • Add the trigger: search for Request, pick When an HTTP request is received. Give it a schema so downstream tokens are typed.
  • Add Parse JSON. Yes, the trigger schema already types things. Doing it explicitly makes the failure mode visible when you demo a malformed payload.

Building the Agent Steps

  • Drop in the agent action. System message, roughly: You triage failed integration messages. Classify severity as high, medium or low. Decide whether the failure is retryable. Use the runbook knowledge source before you answer. If the runbooks do not cover this error code, say so explicitly and set severity to medium. Never invent a runbook reference. Return structured output only.
  • Attach a knowledge base. Agent panel, Knowledge tab, Add knowledge source, Document Upload, upload three or four runbook pages. Knowledge bases are private preview, so this may not be enabled on your project yet. Azure AI Search is the fallback if you already maintain an index.
  • Add tools inside the agent boundary. Keep it to three or four. Toolsets of three to seven beat toolsets of twenty. Write the descriptions like docstrings, because the model’s reasoning is only as good as they are.
    • lookup_error_code as an HTTP action against your error catalogue. Inside it, read the argument with @agentParameters('errorCode').
    • get_recent_failures as a SQL or HTTP action, so the agent can see whether this is a one off or the fortieth today.
    • code_interpreter toggled on in the Parameters tab, for counting and grouping. It is JavaScript only, has no network access and no filesystem, so do not ask it to fetch anything.
  • Set the iteration bound in the Settings tab. Six is plenty here. This is your cost fuse.
  • Branch on the structured output, not the prose:
   @outputs('Triage_Agent')['structuredOutput']?['severity']

High goes to a ticket and an on call ping. Retryable goes back on the queue. Everything else goes into a digest.

Then click Test your draft, and watch the monitoring tab stream the run live. Real time run history is the single biggest day to day improvement over Standard, and it is the thing your audience will notice first.

The preview reality check

Every launch post is written in the present tense about a future state. Here is the current state, as of this writing.

AreaStatus today
CI/CD and deployment pipelinesMissing. There is no deployment story yet.
Export the whole solution as codeMissing. Versioning exists, but at workflow level.
ARM exportWorkflow code does not appear in the ARM template, and Export Template fails in the Azure portal. You can copy and paste workflow code from the Automation portal.
Conversational workflowsNot supported in Automation today, although they are documented for Consumption and Standard in Logic Apps Labs.
VNet integration and private endpointsContested. See below.
Knowledge basesPrivate preview. Document upload limits, per source token budgets and granular permissions are all still moving.
Sandbox input files.txt and .md only in private preview. CSV needs a contentType set by hand in code view.
Code InterpreterJavaScript only. No network. No filesystem. Per execution timeout. Sized for transformation, not compute.
Model supportNot every model works yet. Marczak reports workflows failing on GPT 5.1 that ran on 4.1, and the same workflows working on Standard.
PricingNot finalised. Third party posts quoting specific meters are running ahead of what Microsoft has published.
RegionsAn initial set at launch, with more rolling out. Check before you promise anything.

The VNet discrepancy, because it matters

Microsoft’s launch post describes virtual network integration and private endpoints as day zero enterprise capability. The same post lists “VNet support and private endpoints” in its coming soon section. An MVP testing the preview reports it as missing.

I am not calling anyone wrong. I am saying that if you work in a regulated sector, this is the one line item you must verify in your own tenant before it appears on any roadmap slide. For a health insurer, “reaching internal systems without exposing them to the internet” is not a feature. It is the precondition for the conversation.

The governance question I opened with

Apps are private by default. That is deliberate and, for personal automations connected to someone’s own mailbox or OneDrive, it is correct. Project Owners and Contributors see app name, owner, creation date and last modified. They cannot read workflow contents, connections or run history.

Now put that in an enterprise. Three consequences follow.

  • Auditability. Your compliance function cannot answer “what does this automation do and what does it touch” from the governance view. Someone has to be granted app scope access, per app, by the owner. That is a manual process with no obvious escalation path short of the Project Owner deleting the app.
  • Orphaned apps. When an owner leaves, existing collaborators keep access and only the Project Owner can delete the app. Nobody inherits the ability to read it. In a large organisation, that is a slow accumulation of automations nobody can review and nobody dares remove.
  • Shadow integration. The value proposition is that business teams build their own automations. The governance model means the platform team cannot see what they built. Both statements are true at once. That is not a bug, but it is a policy decision your organisation has to make deliberately rather than discover eighteen months in.

My working position: treat projects as the tenancy unit and map them to a business domain with a named owner and a named deputy. Require that anything touching regulated data lives in a project where the platform team holds app scope Contributor from day one. Write that down before the first workflow ships, not after.

Logic Apps Automation: What I would ask the product team for

Short list, in priority order, from someone who wants to put this in production.

  1. A deployment story. CI/CD and a full project definition as code. Without it, Automation is a place to build, not a place to run a regulated workload. This is the single blocker.
  2. An audit read role. A project scope role that can read workflow definitions and connection metadata across apps, without run history or payloads. Privacy by default and auditability are not in conflict if the role model is granular enough.
  3. Ownership transfer. Reassigning an owner should not require deleting the app.
  4. A published model support matrix for the Automation SKU, with the failure mode visible in the designer rather than at runtime.
  5. Clarity on VNet and private endpoints in preview, stated once, in one place, with the region list.
  6. Project level connector policy shipped early. Being able to block a connector class across a project is what lets a platform team say yes to self service.

Points 1 and 2 are the difference between an interesting preview and something an enterprise architecture board approves.

Logic Apps Automation: Where this is the wrong answer

Because it will not be the right answer everywhere, and pretending otherwise helps nobody.

  • You already run a mature Standard estate. Do not migrate. The engine is the same, the value is the experience, and you would trade a working CI/CD pipeline for one that does not exist yet.
  • SAP, EDI or B2B. Standard. Not close.
  • Predictable, sustained, high throughput load. Standard is more cost efficient and you keep capacity control.
  • Strict data residency or network isolation requirements that you cannot verify today. Wait for the VNet story to settle.
  • Your problem is deterministic. If the steps are known up front and you need exact repeatable behaviour, an agent adds latency, cost and a non deterministic failure mode in exchange for flexibility you do not need. Use a plain workflow.
  • You need conversational agents. Not in Automation yet. Consumption and Standard have that documented today.

What I would do on Monday

If you are a practitioner: get a project, build the triage scenario above or another scenario, and spend your time in the Chat tab and the retrieval view rather than the designer. The designer is pleasant and unsurprising. The observability is where the new value actually is.

If you are a decision maker: do not fund a migration. Fund one team, one project, one non regulated workflow, and a written answer to the ownership and audit question before the second team asks for access. The technology is further along than the operating model, and the operating model is the part you own.

Automation is in public preview. Treat it exactly like one, and it is the most interesting thing to happen to Logic Apps since Standard.

Sources