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.
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.
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.
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.
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.
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:
Scenario
Where I would put it today
SAP, EDI, B2B, high throughput API orchestration
Standard
Central integration platform, predictable 24×7 load
Standard
Simple event driven automation, low volume
Consumption
AI and agent workloads with bursty traffic
Automation
Long idle periods with traffic spikes
Automation
Departmental workflow owned by a business team
Automation, with the caveats in section 5
Personal productivity inside Microsoft 365
Power 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.
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:
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.
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.
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.
Area
Status today
CI/CD and deployment pipelines
Missing. There is no deployment story yet.
Export the whole solution as code
Missing. Versioning exists, but at workflow level.
ARM export
Workflow 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 workflows
Not supported in Automation today, although they are documented for Consumption and Standard in Logic Apps Labs.
VNet integration and private endpoints
Contested. See below.
Knowledge bases
Private 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 Interpreter
JavaScript only. No network. No filesystem. Per execution timeout. Sized for transformation, not compute.
Model support
Not every model works yet. Marczak reports workflows failing on GPT 5.1 that ran on 4.1, and the same workflows working on Standard.
Pricing
Not finalised. Third party posts quoting specific meters are running ahead of what Microsoft has published.
Regions
An 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.
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.
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.
Ownership transfer. Reassigning an owner should not require deleting the app.
A published model support matrix for the Automation SKU, with the failure mode visible in the designer rather than at runtime.
Clarity on VNet and private endpoints in preview, stated once, in one place, with the region list.
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.
The Citadel APIM gateway policies on Azure are doing the heavy lifting silently in every post in this series. We deployed the hub, connected a tool-calling agent, added conversation persistence, and demonstrated the kill switch. Throughout all of that, every chat completion request passed through these policies. They counted every token. They produced all usage telemetry. This post opens the hood and examines all five Citadel APIM gateway policy layers in Azure token rate limiting, semantic caching, content safety routing, cost attribution, and PII redaction with the actual policy XML, live demonstration results, and honest debugging sessions included.
Where the Citadel APIM Gateway Policies Live in Azure
The Citadel hub deploys two types of policies.
API-level policies apply to all operations on the Azure OpenAI API for every chat completion, embedding, and batch request. These contain the token-limiting, usage-tracking, and routing logic.
Policy fragments serve as reusable blocks that you include by reference. The hub uses fragments for AAD authorization (aad-auth), load balancing (openai-backend-pool), and dynamic throttling (dynamic-throttling-assignment).
In the portal, find them at apim-wpvlimv4ngkns → APIs → Azure OpenAI Service API → All operations → Policies.
The full policy XML is also in the repo at github.com/Azure-Samples/ai-hub-gateway-solution-accelerator under infra/modules/apim/policies/.
APIM natively supports rate limiting on the number of requests per time window. The hub repurposes this to manage capacity based on tokens — the azure-openai-token-limit policy limits how many tokens per minute a subscription can consume, using a counter keyed to the APIM subscription.
counter-key=”@(context.Subscription.Id)” — the counter is per APIM subscription, so each spoke gets its own independent token budget.
tokens-per-minute=”10000″ — 10,000 TPM limit per subscription; adjust per spoke based on workload.
estimate-prompt-tokens=”true” — APIM estimates prompt tokens before the response arrives, enabling proactive rate limiting rather than post-hoc tracking.
tokens-consumed-variable-name=”TotalConsumedTokens” — stores the running count for use by downstream policies (cost attribution reads this variable).
Demonstration
The Citadel hub actually applies two independent token limit policies at different scopes, and tracing the real behaviour reveals an important lesson about how APIM evaluates them.
Scope 1 — subscription-level, per deployment. The product policy on oai-retail-assistant assigns a different tokens-per-minute value based on the targeted deployment.
Both policies execute on every request, each maintaining its own counter. Whichever limit you hit first governs the response, regardless of which one you configured.
Live test — three rapid requests against the chat deployment with the subscription-level limit set to 50 TPM:
REQUEST 2 — Status: 429, Body: “Token limit is exceeded. Try again in 51 seconds.”
REQUEST 3 — Status: 429, Body: “Token limit is exceeded. Try again in 51 seconds.”
Notice that Remaining-Tokens: 14940 on the first request comes from the product-level counter (15000 TPM, barely touched). The 429 on request 2, however, comes from the subscription-level counter for the chat deployment, which only allows 50 tokens per minute, exhausted after a single 15-token call. The visible remaining-tokens header reflects whichever counter last wrote to it, which can be misleading if you assume there is only one limit in play.
When multiple azure-openai-token-limit policy elements exist at the API level, product level, and per deployment in a choose block, they are all evaluated for each request. In addition, the most restrictive policy that triggers first decides the outcome. Furthermore, to debug rate limit issues, check all policy scopes: API-level, product-level, and deployment-specific branches. A Named Value in one scope doesn’t affect a hardcoded limit in another.
Citadel APIM Gateway Policy 2 — Semantic Caching
What It Does
Semantic caching intercepts requests before they reach Azure OpenAI and checks for previously answered semantically similar questions. If it finds a match above the configured similarity threshold, it returns the cached response immediately, using zero tokens and ensuring near-zero latency.
The Policy XML
The lookup and store directives belong in different policy sections — lookup runs on the inbound request, store runs on the outbound response after a successful call.
score-threshold=”0.8″ — similarity must be 80% or higher for a cache hit. Lower values increase hit rate but risk returning mismatched responses.
embeddings-backend-id=”openai-backend-0″ — points to the backend that hosts the embeddings deployment used for cache lookup. The embeddings model itself is configured on the backend, not as a policy attribute — embeddings-model is not a valid attribute on this policy element and will fail schema validation if added.
embeddings-backend-auth=”system-assigned” — the embeddings backend call is authenticated via the APIM instance’s system-assigned managed identity.
ignore-system-messages=”true” — only user messages are used for cache key generation, not system prompts.
max-message-count=”5″ — only the last 5 messages in a conversation are used for cache lookup.
duration=”600″ — cached responses expire after 10 minutes.
Demonstration
First request — cache miss:
pythonagent.py
# Question: What is the weather like in Stockholm right now?
# Response time: 1.2 seconds
Second request — cache hit:
python agent.py
# Question: What is the weather in Stockholm today?
# Response time: 45ms
The second question resembles the first semantically, but it’s not an exact match. The 0.8 threshold identifies it as a cache hit, allowing the system to return the response from the cache in 45ms without making an Azure OpenAI call or using any tokens. For a conversational agent that often encounters similar questions, semantic caching can lower token consumption by 20–40%.
Production Consideration
The cache shares its data across all subscribers to the same APIM product. If two spokes utilize the same APIM gateway, Spoke A’s cached response can serve Spoke B for a similar inquiry. To protect sensitive data, configure the cache key to be scoped per subscription by adding @(context.Subscription.Id).
Pitfall: Policy Section Placement and Schema Validation
When adding semantic caching for the first time, you may encounter two common schema errors. First, placing azure-openai-semantic-cache-store in the inbound section, along with the lookup policy, results in the error: “Policy is not allowed in this section.” Second, you’ll find that embeddings-model is not a declared attribute on azure-openai-semantic-cache-lookup. Instead of being specified directly in the policy, the embeddings deployment is retrieved from the backend referenced by embeddings-backend-id. You can quickly identify both errors when you save the policy in the portal, which provides the fastest validation of your XML before deployment.
The hub routes every request through Azure AI Content Safety before it reaches Azure OpenAI. It inspects both the user prompt and the model response for harmful content in four categories: hate, self-harm, sexual, and violence. The system blocks any requests or responses that exceed the configured severity thresholds.
backend-id=”content-safety-backend” — routes to the cog-consafety-wpvlimv4ngkns Azure AI Content Safety instance deployed in the hub.
shield-prompt=”true” — enables Prompt Shields, which additionally detects jailbreak attempts and prompt injection on top of standard content categories.
categories output-type=”FourSeverityLevels” — selects the four-level severity scale (0, 2, 4, 6) rather than the eight-level scale; each category child element sets its own threshold independently.
category name=”…” threshold=”2″ — one element per category (Hate, SelfHarm, Sexual, Violence), each can have a different threshold. A threshold of 2 blocks low severity and above; omitting a category leaves it unchecked.
You can reference custom Azure AI Content Safety blocklist IDs in an optional blocklists element to always block organization-specific terms, regardless of their severity scoring.
Pitfall: Element Name and Schema
The element is llm-content-safety, not azure-content-safety — using the wrong name produces “There is no policy matching element name” on save. The schema is also structural rather than attribute-based: categories are child category elements inside a categories block, not a flat comma-separated categories=”…” attribute. Both errors surface immediately in the portal policy editor, which validates the XML before allowing a save.
Demonstration
Normal request — content safety passes silently:
pythonagent.py
# Question: What is the weather like in Stockholm?
# Answer: The weather in Stockholm is overcast...
The Policy
The llm-content-safety policy does not add a confirmation header on a passing request — the absence of a block response is the only signal that the prompt and response cleared all four category thresholds. To confirm the policy actually ran, check Application Insights:
requests
|wheretimestamp> ago(10m)
|where resultCode =="200"
| project timestamp, name, resultCode, duration
|orderbytimestampdesc
A 200 with normal duration confirms the request passed through llm-content-safety without being blocked.
Harmful content
Blocked request — harmful content detected:
# Modify agent.py to send a harmful prompt, then run:
pythonagent.py
# openai.BadRequestError: Error code: 400
# 'Request failed content safety check.'
Requests block at the gateway before they reach Azure OpenAI, ensuring that these blocked requests consume no tokens. In Application Insights, you observe the block as a 400 response with near-zero duration, reflecting the same signature pattern seen with the kill switch layers mentioned in the previous post.
Every request that completes successfully generates a usage event sent to Azure Event Hub. A Logic App deployed in the hub consumes these events and writes structured documents to the Cosmos DB ai-usage-container. Each document contains token counts, model version, gateway region, APIM subscription name, and timestamp, giving you per-subscription cost attribution without any agent-side code changes.
The Policy XML
The hub deploys three named loggers, visible via az rest against the APIM management API: appinsights-logger for Application Insights telemetry, usage-eventhub-logger for the cost attribution pipeline described here, and a separate pii-usage-eventhub-logger for PII-specific event logging tied to the redaction policy described later in this post. The logger-id attribute on log-to-eventhub must match one of these exactly — using a placeholder or guessed name produces “Logger not found” when saving the policy.
var usage = response.Body.As<JObject>(true)?["usage"];
return new JObject(
new JProperty("id", context.Response.Headers.GetValueOrDefault("x-request-id", Guid.NewGuid().ToString())),
new JProperty("timestamp", DateTime.UtcNow.ToString("o")),
new JProperty("subscriptionId", context.Subscription?.Id),
new JProperty("productName", context.Product?.Name),
new JProperty("targetService", "chat.completion"),
new JProperty("model", response.Body.As<JObject>(true)?["model"]?.ToString()),
new JProperty("gatewayName", context.Deployment.ServiceName),
new JProperty("gatewayRegion", context.Deployment.Region),
new JProperty("RequestIp", request.IpAddress),
new JProperty("promptTokens", usage?["prompt_tokens"]?.ToObject<int>() ?? 0),
new JProperty("responseTokens", usage?["completion_tokens"]?.ToObject<int>() ?? 0),
new JProperty("totalTokens", usage?["total_tokens"]?.ToObject<int>() ?? 0),
new JProperty("backendId", context.Variables.GetValueOrDefault<string>("backendId")),
new JProperty("deploymentName", context.Request.MatchedParameters["deployment-id"])
).ToString();
}
</log-to-eventhub>
Key fields:
context.Subscription?.Id and context.Product?.Name — the APIM subscription and product name used for the request. In a multi-spoke setup, each spoke has its own APIM product making cost attribution per initiative automatic.
usage?[“prompt_tokens”] / usage?[“completion_tokens”] — extracted directly from the Azure OpenAI response body.
context.Deployment.Region — the gateway region (Sweden Central) for data residency auditing.
context.Variables.GetValueOrDefault<string>(“backendId”) — which Azure OpenAI backend served the request, critical for multi-region deployments.
Demonstration
Run the agent:
python agent.py
Then query the hub Cosmos DB in the portal (cosmos-wpvlimv4ngkns → Data Explorer → ai-usage-container → Items):
SELECT TOP 5 c.timestamp, c.productName, c.model,
c.promptTokens, c.responseTokens, c.totalTokens,
c.gatewayRegion, c.deploymentName
FROM c ORDERBY c._ts DESC
A real document from the deployed hub looks like this:
{
"timestamp":"6/30/2026 11:26:49 AM",
"productName":"OAI-HR-Assistant",
"model":"gpt-4o-2024-11-20",
"promptTokens":93,
"responseTokens":56,
"totalTokens":149,
"gatewayRegion":"Sweden Central",
"deploymentName":"chat"
}
Each agent run produces two documents, one for the tool decision call and one for the synthesis call. The totalTokens across both documents is the true per-conversation cost. The productName field maps directly to the APIM product the calling subscription belongs to, in this example OAI-HR-Assistant, distinct from any other product sharing the same hub.
FinOps in Practice
In production with multiple spokes, filter by productName to get per-initiative cost:
SELECT c.productName,
SUM(c.totalTokens)as totalTokens,
COUNT(1)as requestCount
FROM c
WHERE c.timestamp >="2026-06-01T00:00:00Z"
GROUPBY c.productName
Run against the live hub, this returns a clean per-product summary:
[
{
"productName":"Portal-Admin",
"totalTokens":3865,
"requestCount":32
},
{
"productName":"OAI-HR-Assistant",
"totalTokens":3035,
"requestCount":31
}
]
Two distinct products, each with an independently aggregated token total and request count, no additional instrumentation required beyond the log-to-eventhub policy already in place. This is your direct FinOps input per AI initiative, per month, ready to feed into a PowerBI report or a monthly cost allocation process.
Citadel APIM Gateway Policy 5 — PII Redaction
What It Does
The hub detects personally identifiable information — names, email addresses, phone numbers, IBAN numbers, and other entity types — in conversation content and logs a redacted version to a dedicated Event Hub logger. The original request still reaches Azure OpenAI unmodified; only the logged version is clean.
There Is No Built-In Policy Element for This
The blog draft for this post originally referenced an azure-openai-pii-removal-logging policy element, on the assumption that APIM ships a built-in PII redaction policy the same way it ships azure-openai-token-limit and azure-openai-semantic-cache-lookup. It does not. Saving a policy referencing that element name fails immediately with “There is no policy matching element name,” the same class of error encountered earlier with azure-content-safety versus the correct llm-content-safety.
Unlike content safety, however, there is no equivalent built-in alternative for PII detection at the time of writing. The hub’s pii-usage-eventhub-logger, visible alongside usage-eventhub-logger and appinsights-logger when listing loggers via the APIM management API, exists as infrastructure for this purpose, but the policy that populates it has to be built explicitly using send-request to call Azure AI Language Service directly, then log-to-eventhub to write the result.
The Policy XML
This belongs entirely in outbound, immediately after the cost attribution log-to-eventhub block. PII detection runs on the response side rather than inbound because the goal is to log a redacted record of the conversation, not to block or alter the request itself, context.Request.Body remains accessible in the outbound pipeline, so the original user message can still be analysed at this stage.
PII detection — outbound section, after cost attribution:
var piiResult = ((IResponse)context.Variables["piiDetectionResponse"]).Body.As<JObject>(true);
var redactedText = piiResult?["results"]?["documents"]?[0]?["redactedText"]?.ToString() ?? "[pii-detection-unavailable]";
var entityCount = piiResult?["results"]?["documents"]?[0]?["entities"]?.Count() ?? 0;
return new JObject(
new JProperty("timestamp", DateTime.UtcNow.ToString("o")),
new JProperty("subscriptionId", context.Subscription?.Id),
new JProperty("productName", context.Product?.Name),
new JProperty("redactedText", redactedText),
new JProperty("piiEntitiesFound", entityCount)
).ToString();
}
</log-to-eventhub>
Line by line:
send-request mode=”new” — fires an independent outbound call to Azure AI Language Service rather than reusing the current request/response context. ignore-error=”true” means a Language Service outage does not fail the agent’s actual response, only the PII logging step is skipped.
requestBody[“messages”]?.Last?[“content”] — extracts the most recent user message from the original request body for analysis, since that is where PII is most likely to appear.
The Language Service PiiEntityRecognition endpoint returns both a redactedText field (PII replaced with asterisks) and an entities array listing every detected entity and its category.
log-to-eventhub logger-id=”pii-usage-eventhub-logger” — writes the redacted text and entity count to the dedicated PII logger, kept separate from the general usage logger so PII-related audit records can be access-controlled independently.
Two Named Values must exist before this policy validates:
azapimnvcreate\
--resource-grouprg-ai-hub-gateway-dev\
--service-nameapim-wpvlimv4ngkns\
--named-value-idlanguage-service-name\
--display-name"language-service-name"\
--value"cog-language-wpvlimv4ngkns"\
--secretfalse
azapimnvcreate\
--resource-grouprg-ai-hub-gateway-dev\
--service-nameapim-wpvlimv4ngkns\
--named-value-idlanguage-service-key\
--display-name"language-service-key"\
--value"<your-language-service-key>"\
--secrettrue
Retrieve the Language Service key:
azcognitiveservicesaccountkeyslist\
--namecog-language-wpvlimv4ngkns\
--resource-grouprg-ai-hub-gateway-dev\
--querykey1-otsv
Demonstration
Request containing PII:
question = "What is the weather near Jan Janssen who lives at Keizersgracht 123 Amsterdam?"
First attempt, the policy ran but logged the wrong data. The send-request call to Azure AI Language Service fired correctly and returned 200 every time, confirmed via Application Insights dependency tracking:
Yet the documents landing in pii-usage-container showed no redactedText field at all, only promptTokens, gatewayName, backendId, and the rest of the cost attribution schema:
{
"id":"30db8c4b-ce45-4695-9078-e357405845bc",
"subscriptionId":"oai-hr-assistant-sub-01",
"productName":"OAI-HR-Assistant",
"model":"gpt-4o-2024-11-20",
"promptTokens":109,
"responseTokens":21,
"gatewayName":"apim-wpvlimv4ngkns.azure-api.net",
"backendId":"openai-backend-0"
}
The cause turned out to be a copy-paste error in the log-to-eventhub block itself. The comment above it correctly read “PII detection and redacted logging,” but the JObject construction inside was a verbatim duplicate of the cost attribution payload from usage-eventhub-logger, it never referenced context.Variables[“piiDetectionResponse”] at all. The Language Service call succeeded and its result sat in a context variable, completely unused, while the logger faithfully wrote the wrong document every time. Every dependency call returned 200; every Cosmos DB write succeeded; nothing in the telemetry indicated a problem. The only way to catch it was reading the document schema in Cosmos DB and noticing it matched the usage container rather than containing redacted text.
The fix was correcting the log-to-eventhub body to actually read the stored piiDetectionResponse variable:
var piiResult = ((IResponse)context.Variables["piiDetectionResponse"]).Body.As<JObject>(true);
var redactedText = piiResult?["results"]?["documents"]?[0]?["redactedText"]?.ToString() ?? "[pii-detection-unavailable]";
var entityCount = piiResult?["results"]?["documents"]?[0]?["entities"]?.Count() ?? 0;
return new JObject(
new JProperty("id", context.Response.Headers.GetValueOrDefault("x-request-id", Guid.NewGuid().ToString())),
new JProperty("timestamp", DateTime.UtcNow.ToString("o")),
new JProperty("subscriptionId", context.Subscription?.Id),
new JProperty("productName", context.Product?.Name),
new JProperty("redactedText", redactedText),
new JProperty("piiEntitiesFound", entityCount)
).ToString();
}</log-to-eventhub>
After the fix, the same question produced the correct redacted record:
{
"id":"419321ce-0544-4908-a2ac-9ce10f80aaba",
"timestamp":"2026-06-30T13:12:24.8989851Z",
"subscriptionId":"oai-hr-assistant-sub-01",
"productName":"OAI-HR-Assistant",
"redactedText":"What is the weather near *********** who lives at ***************************?",
"piiEntitiesFound":2
}
Two entities detected and redacted, the person’s name and the street address, while the sentence structure remains intact for log readability. A second request, where the agent’s tool call itself failed to resolve the address, still produced a correctly redacted record of the error message:
{
"redactedText":"{\"error\": \"Location '***************************' not found.\"}",
"piiEntitiesFound":1
}
This confirms PII redaction applies consistently regardless of whether the underlying tool call succeeds, the policy operates on the original user message, independent of how the agent’s downstream logic handles it.
In Cosmos DB usage documents, unaffected: the cost attribution document written by usage-eventhub-logger still contains only token counts and metadata, never request body content, so PII redaction applies exclusively to the dedicated pii-usage-container stream.
Pitfall: A Logger Existing Does Not Mean It Logs the Right Thing
The most instructive failure in this section was not a missing policy element or a schema validation error, it was a policy that validated, deployed, and executed successfully while silently logging the wrong payload. Every signal that normally indicates “this is working” was green: the send-request dependency call returned 200, the log-to-eventhub write succeeded, and documents appeared in Cosmos DB on schedule. The only way to catch the bug was to inspect the actual field names in the logged document and notice they matched a different policy’s output entirely. When wiring up custom logging policies, always verify the logged document shape directly in the data store, a successful HTTP status code on a dependency call says nothing about whether its result was ever used downstream.
Pitfall: Two send-request Round Trips Add Latency
Every request now makes an additional outbound call to Azure AI Language Service before the agent’s response is returned to the caller, since send-request blocks until it completes (or times out at 10 seconds with ignore-error=”true”). For latency-sensitive workloads, consider moving PII detection to an asynchronous pattern, log raw request IDs to Event Hub immediately, then run PII detection as a separate downstream process reading from the Event Hub stream, rather than inline in the request path.
The Complete Policy Execution Order
Understanding the order in which policies execute is critical for troubleshooting. APIM processes policies in this sequence.
This order means a request blocked by the kill switch never reaches token rate limiting, content safety, or Azure OpenAI. A request blocked by content safety never reaches cost attribution or PII logging, no usage document and no redacted-text document are created for blocked requests.
Validating All Five Policies in Application Insights
Use this KQL query to see the policy execution evidence in one view:
tokensConsumed — running token count from rate limiter.
remainingTokens — remaining budget for this subscription.
There is no dedicated content safety column because llm-content-safety does not emit a custom telemetry property, its outcome is entirely reflected in resultCode. A 400 with near-zero duration is the signature of a content safety block, the same pattern used to identify kill switch activations in the previous post.
For PII detection specifically, dependency tracking shows whether the Language Service call fired:
dependencies
|wheretimestamp> ago(24h)
|where target contains "cognitiveservices"and name contains "analyze-text"
A 200 here only confirms the call succeeded, it does not confirm the result was logged correctly downstream. Cross-check against the actual documents in pii-usage-container to verify the redactedText field is present and populated, not just that the dependency call returned successfully.
Pitfalls Summary
Token rate limiting doesn’t work for streaming. Fix: use non-streaming endpoints for accurate token counting.
Semantic cache returns stale weather data. Fix: reduce duration to 60 seconds for time-sensitive tool calls.
Content safety blocks valid medical terminology. Fix: raise threshold to 4 for healthcare-specific deployments and validate against your use case.
azure-content-safety element does not exist. Fix: use llm-content-safety with categories and category child elements, not flat attributes.
azure-openai-pii-removal-logging element does not exist. Fix: no built-in policy exists, implement via send-request to Language Service plus a custom log-to-eventhub.
The logger successfully writes but records incorrect data. To fix this, a 200 status on the send-request and a successful write to Event Hub do not guarantee that the result was actually used. Instead, verify the logged document schema directly in Cosmos DB.
The system misses redacting Dutch-language names. To fix this issue, optimize the analyze-text request by setting the language to “nl” and testing it with representative Dutch inputs, instead of using “en,” which is optimized for English.
Cost attribution missing for some requests. Fix: check Event Hub to Logic App pipeline; ingestion lag of 2 to 5 minutes is normal.
To ensure high compliance in production, always surface failures in content safety and PII detection instead of silently bypassing them with the on-error-action or ignore-error=”true” settings.
Conclusion
The five APIM policies in the Citadel hub, token rate limiting, semantic caching, content safety, cost attribution, and PII redaction, collectively implement enterprise AI governance at the gateway layer. None of them require changes to agent code. None of them require spoke involvement. All of them apply to every request from every agent that routes through the hub, regardless of which spoke deployed it.
Furthermore, they compose cleanly: a request that hits the semantic cache never reaches token rate limiting, content safety, or Azure OpenAI. As a result, cached responses consume zero tokens, zero content safety budget, and zero Azure OpenAI quota. Similarly, a request blocked by content safety produces no cost attribution event and no PII redaction record.
This composability is what makes the Citadel APIM hub a governance layer rather than just a proxy. The policies work together, in a defined order, to enforce the enterprise AI control plane pattern across every governed workload, though, as the PII redaction section makes clear, composability and correct execution are not the same thing. A policy chain can validate, deploy, and run green across every dependency call while still logging the wrong data. The only reliable verification is checking the actual data that lands in the store, not the status codes along the way.
A comment on the first post in this series asked why AI Foundry Spoke model deployment happens at all in the Citadel pattern, a question worth answering properly in its own post rather than buried in a reply thread.
Great article as usual! Just wondering, in the given Governance Hub & Agent Spoke architecture, what is the purpose of deploying the models both to the Spoke and the Hub? Shouldn’t they only be deployed to the Hub and provided from there?
That’s a sharp question, and it points at a real tension in the Citadel pattern that the original post didn’t call out explicitly enough. Here’s the direct answer, followed by the reasoning behind it.
The short answer
No, they shouldn’t both serve your application’s inference needs. Only the Hub deployment should. The model deployment sitting in the Spoke exists today because of how Azure AI Foundry’s Agent Service currently works, not because the architecture intends a second, governance-free inference path.
That distinction matters, so it’s worth walking through why the Spoke deployment is there at all.
Why AI Foundry Spoke Model Deployment Happens at All
In the ideal version of the Citadel pattern, every model call flows through the Hub’s APIM gateway. That’s the entire point of centralizing governance in one place. It’s what the rest of the series demonstrated: every agent call routed through apim-wpvlimv4ngkns, with token tracking, content safety, cost attribution, and the kill switch all enforced at that single choke point.
The Spoke still ends up with its own local model deployment because the AI Foundry Agent Service needs one for two reasons. This is a direct consequence of how the AI Landing Zone Bicep templates provision the Spoke, not a choice made anywhere in this series.
It powers the Agent Service’s own internal capabilities. Thread management, agent orchestration, and built-in tools like Code Interpreter or File Search (when enabled) call the model directly, through Foundry’s own runtime, rather than through any external endpoint you control. That runtime doesn’t route through APIM. It talks to whatever model deployment sits alongside it in the same project.
It satisfies the Foundry project’s provisioning requirements. A Foundry project currently expects an associated model deployment to exist as part of setting up the project, even if your actual application traffic never calls that deployment directly.
Neither of these is a governance decision. They’re artifacts of how the Agent Service is architected right now.
Two paths, not one
The practical result is that a Citadel deployment ends up with two separate paths to a model, and they serve different purposes.
Application traffic should only ever reach a model through APIM in the Hub. The Spoke’s local deployment exists for Foundry’s internal agent runtime, not for your code to call directly.
The Spoke’s local deployment exists for Foundry’s own internal agent runtime. It’s not meant to see your production traffic, and if it does, none of the governance you built in the Hub applies to those calls.
The Hub’s deployment, reached through APIM, is what your application code should use. That’s what we wired up explicitly in Part 2 of this series, with the standard OpenAI SDK pointed at the gateway rather than directly at the Foundry endpoint. The Hub itself is built on the AI Hub Gateway Solution Accelerator, and its AI gateway capabilities are exactly what give APIM the token metering, content safety, and audit trail features this series has leaned on throughout.
The second path exists precisely because of a limitation the series already documented. The Agent Service SDK, in its current preview state, doesn’t route its own LLM calls through APIM. It bypasses the gateway entirely, which means using it directly would mean giving up token metering, policy enforcement, and audit trails on every call the agent makes. That’s why Part 2 used the standard OpenAI SDK pointed at APIM instead of the native Agent Service SDK, and it’s the same underlying issue this reader’s question is really about.
What this means in practice
If you’re building on this pattern today, treat the Spoke’s model deployment as infrastructure the platform needs to exist, not as a second inference endpoint your application is allowed to call. Point your application code at the Hub, through APIM, every time. Leave the Spoke deployment alone to do the job Foundry needs it for internally, and don’t build anything that calls it directly for your own traffic.
If you’re reviewing someone else’s Citadel-pattern deployment, this is worth checking explicitly. A model deployment sitting in a Spoke isn’t wrong by itself, but it’s worth confirming nothing in the application is quietly calling it and skipping the gateway.
Where this is heading
I’d expect this to tighten up as the Agent Service SDK matures out of preview and gains native APIM routing support. When that happens, the two-path situation described here becomes a one-path situation, and the Spoke deployment stops being something you need to actively route around.
Once the Agent Service SDK supports native APIM routing, both application and agent traffic converge on a single governed path.
Until then, the answer to the original question stands: deploy to the Hub, govern everything through APIM, and treat the Spoke’s model deployment as plumbing the platform needs rather than a second front door.
Thanks to the reader who asked the original question. It’s exactly the kind of detail that’s easy to leave implicit in an architecture diagram and much more useful said out loud.
In the previous post we deployed a working Microsoft Foundry Citadel Platform on Azure Sweden Central, a Governance Hub built on Azure API Management and an Agent Spoke built on Azure AI Foundry. We validated the setup with a raw chat completion call through the APIM gateway. That proved the plumbing works. This post takes the next step: connecting a real tool-calling agent to the Microsoft Foundry Citadel Platform on Azure, using the Open-Meteo weather API as a tool, and showing that every LLM call flows through the hub’s governance layer.
The agent is built with the standard Azure OpenAI SDK pointed directly at the Citadel APIM gateway. It uses a custom function tool that calls the Open-Meteo API to retrieve real current weather data for any location. The governance hub intercepts all traffic: content safety policies fire, token usage is tracked, and telemetry flows into Application Insights. This is the Microsoft Foundry Citadel Platform doing what it is designed to do.
What We Build
The flow looks like this:
End-to-end flow of a tool-calling agent on the Microsoft Foundry Citadel Platform in Azure Sweden Central, the Python agent routes both LLM calls through the APIM Governance Hub, executes the get_weather tool against Open-Meteo, and receives a grounded response, with all traffic captured in Application Insights and Cosmos DB.
Two LLM calls flow through APIM per agent run: the tool decision call and the synthesis call. Both are governed, appear in Application Insights, and contribute to Cosmos DB usage tracking.
Why Open-Meteo and Why the Standard OpenAI SDK
The original plan was to use the Azure AI Foundry Agent Service SDK with Bing Search grounding. Two blockers emerged:
Bing Search SKU eligibility: The Grounding with Bing Search resource (G1 SKU) requires Pay-As-You-Go or EA subscriptions and is not available on MVP or MSDN subscriptions.
AI Foundry Agent Service routing: The azure-ai-projects SDK routes LLM calls through the AI Foundry project’s internal endpoint (aif-tggi2gmkw22w4.openai.azure.com) rather than through APIM, bypassing the governance layer. In addition, even after adding APIM as a connected resource in the AI Foundry portal, the Agent Service does not honor it for model routing in the current preview version.
The solution, therefore, is to use the standard OpenAI Python SDK pointed directly at the APIM gateway endpoint. This guarantees that all traffic flows through the hub; consequently, the tool-calling loop is implemented explicitly in Python, and the governance telemetry is fully captured in Application Insights.
Open-Meteo is a free, open-source weather API; therefore, it requires no API key and returns structured JSON weather data. Additionally, it serves as a clean stand-in for any external API your agents might call in production.
Prerequisites
From the previous post you should have:
Hub deployed in rg-ai-hub-gateway-dev with APIM gateway URL https://apim-wpvlimv4ngkns.azure-api.net and subscription key
Spoke deployed in rg-ai-spoke-dev with App Config appcs-tggi2gmkw22w4 containing APIM_GATEWAY_URL and APIM_SUBSCRIPTION_KEY
Your principal ID with App Configuration Data Reader role on the spoke App Config
For this post you additionally need Python 3.11 or later installed locally.
Step 1 — Set Up the Python Environment
mkdir citadel-agent && cd citadel-agent
python -m venv .venv
# Windows
.venv\Scripts\activate
pip install openai
pip install azure-appconfiguration
pip install azure-identity
pip install requests
Step 2 — Read Configuration from App Config
Create config.py using Set-Content to avoid BOM issues on Windows:
All four keys should return truncated values. If you get a 403, wait 2–5 minutes for role assignment propagation and retry.
Pitfall: Always Use WriteAllLines for Python Files on Windows
Out-File -Encoding utf8NoBOM and @"..."@ | Out-File both add a BOM on some Windows PowerShell versions, causing Python to throw SyntaxError: Non-UTF-8 code starting with '\xff'. Use [System.IO.File]::WriteAllLines with [System.Text.UTF8Encoding]::new($false) to write files without BOM.
The AzureOpenAI SDK constructs the full path as {azure_endpoint}/openai/deployments/{model}/chat/completions. If your APIM_GATEWAY_URL in App Config contains /openai at the end, strip it before passing to the client; otherwise, the SDK builds a doubled path (/openai/openai/...) that returns a 500 from APIM. The line apim_base = cfg['APIM_GATEWAY_URL'].rstrip('/').replace('/openai', '') handles this automatically.
After running the agent, check Application Insights in the hub:
az monitor app-insights query `
--app <YourAPIMinstanceName> `
--resource-group rg-ai-hub-gateway-dev `
--analytics-query "requests | where timestamp > ago(10m) | project timestamp, name, resultCode, duration | order by timestamp desc" `
--output table
Pitfall: CLI vs Portal Ingestion Lag
The CLI query hits the Log Analytics store; however, it has a 5–10-minute ingestion lag. In contrast, the Azure Portal Application Insights blade uses a live metrics path and shows results immediately. Therefore, if the CLI returns an empty response, it’s a good idea to check the portal directly, go to the APIM instance → Performance to view requests in real time.
What Governed Traffic Looks Like in the Portal
The Application Insights Performance blade shows two operation types per agent run:
azure-openai-service-api:rev=1 - ChatCompletions_Create — the APIM policy-matched operation, showing the governed calls with content safety applied
POST /openai/openai/deployments/chat/chat/completions — the raw endpoint calls
Each agent run generates two successful requests (tool decision + synthesis), both with response code 200 and latency around 900ms–1.2s for gpt-4o. Failed attempts from earlier endpoint format issues show as 500s and are clearly distinguishable.
Application Insights Performance blade for the Citadel Governance Hub, confirming agent traffic routed through APIM: 9 requests captured, with the governed ChatCompletions_Create operation averaging 1.09 seconds, and all successful calls returning response code 200.
The Azure AI Foundry Agent Service SDK — What We Learned
For completeness, here is a summary of what we discovered when attempting to use the azure-ai-projects SDK before switching to the standard OpenAI SDK:
Issue
Detail
FunctionTool import path
Must import from azure.ai.agents.models, not azure.ai.projects.models
create_thread does not exist
Use create_thread_and_process_run instead
list_messages does not exist
Use client.agents.messages.list(thread_id=...)
MessageRole.ASSISTANT does not exist
Use the string "assistant" directly
enable_auto_function_calls(toolset=...) fails
Parameter is tools=, not toolset=
Function not found error
Call client.agents.enable_auto_function_calls(tools=toolset) before create_agent
Agent traffic bypasses APIM
AI Foundry Agent Service uses its own endpoint resolution — use standard OpenAI SDK pointed at APIM instead
The Agent Service SDK is in active beta development (azure-ai-agents==1.2.0b6 at the time of writing). Expect these APIs to stabilise and the APIM routing issue to be addressed in future versions.
Pitfalls Summary
Pitfall
Fix
Grounding with Bing Search G1 SKU not eligible
Requires Pay-As-You-Go or EA subscription
Bing.Search.v7 CLI creation fails
Resource type moved to Microsoft.Bing/accounts
BOM in Python files on Windows
Use [System.IO.File]::WriteAllLines with UTF8Encoding($false)
APIM endpoint doubles /openai path
Strip /openai from URL before passing to AzureOpenAI client
App Config 403 on first run
Wait 2–5 minutes for role assignment propagation
CLI Application Insights query empty
5–10 minute ingestion lag — check portal Performance blade instead
AI Foundry Agent Service bypasses APIM
Use standard openai SDK pointed directly at APIM gateway
What the Full Citadel Loop Delivers
With the agent running through APIM, every LLM call in the tool-calling loop is governed:
Content Safety — both the user question and the synthesised response pass through Azure AI Content Safety policies configured in APIM.
Token tracking — each of the two LLM calls contributes to the token usage log in Cosmos DB, giving you per-call cost attribution by APIM subscription key. The Cosmos DB ai-usage-container in the hub captures a structured document for each LLM call, including the model version, token counts, gateway region, request IP, APIM subscription name, backend routing, and timestamp. In production, the productName field maps to the APIM subscription key. Aggregating documents by this field gives you direct FinOps reporting per AI initiative.
The Citadel hub Cosmos DB ai-usage-container showing a usage document captured from the tool-calling agent run model gpt-4o-2024-11-20, 70 total tokens, gateway region Sweden Central, routed via apim-wpvlimv4ngkns. Every LLM call through APIM generates a document like this, which serves as the cost attribution and audit trail for enterprise AI governance.
Latency observability — Application Insights captures the duration of every call, making it easy to identify slow tool calls or model latency spikes.
Audit trail — every request is logged with timestamp, operation name, response code, and duration. For a healthcare or financial services context, this is your compliance evidence.
What’s Next
This post wires a tool-calling agent to the Citadel hub using the standard OpenAI SDK. The natural next steps:
Azure AI Foundry Agent Service routing — as the SDK matures, the azure-ai-projects client will likely gain proper APIM gateway support. Watch the azure-ai-agents release notes for updates on connection-based routing.
Conversation persistence — store conversation history in the Cosmos DB conversations container already deployed in the spoke. The App Config key CONVERSATIONS_DATABASE_CONTAINER points to it.
Network isolation — re-enable networkIsolation=true in the spoke parameters to route all traffic through private endpoints.
Multiple tools — extend the agent with additional function tools (document lookup, product catalog, claims system) using the same pattern. Each tool call flows through APIM and is governed identically.
Conclusion
Connecting a real tool-calling agent to the Microsoft Foundry Citadel Platform on Azure requires three components: the standard OpenAI SDK configured to point to the APIM gateway, a function tool with a JSON schema definition, and an explicit tool-call-handling loop. Everything else, governance, content safety, token tracking, and cost attribution, is handled by the Citadel hub automatically.
The path to get here involved navigating several SDK beta rough edges and discovering that the AI Foundry Agent Service bypasses APIM in its current preview form. These are expected friction points with a platform in active development. The governance architecture underneath is sound, the APIM policies work, and the Application Insights telemetry confirms it.
Two LLM calls. Both governed. Both visible. That is what the Citadel hub delivers.
Part 6 covered the security stack for agentic workflows: Easy Auth, Managed Identity, and Key Vault. This final post closes the series with Azure Logic Apps agent loop production operations: how to monitor agent loops with Application Insights, what the pricing model looks like across Standard and Consumption, the key platform limits to be aware of, and how to deploy agentic workflows through a repeatable DevOps pipeline.
By the end of this post, you will have a complete picture of what it takes to run an agentic workflow in production, not just to build one.
Azure Logic Apps agent loop production monitoring with Application Insights
The run history you have used throughout this series is the starting point for understanding what an agent loop did and why. For production workloads you need more: aggregated metrics across multiple runs, structured log queries, alerting on failures, and tracing across distributed systems. Application Insights provides all of this for Standard logic apps.
Enabling Application Insights
If you did not enable Application Insights when you created la-agent-loop, you can add it after deployment:
In the Azure portal, open your la-agent-loop logic app resource
Navigate to Application Insights under Settings in the left sidebar
Click Turn on Application Insights
After the pane updates, click Apply → Yes
Click View Application Insights data to open the dashboard
Application Insights begins collecting telemetry from that point forward; it does not backfill historical run data.
What Application Insights captures for agent loops
For Standard agentic workflows, Application Insights captures enhanced telemetry beyond what the run history provides. Key data points include:
Requests — each workflow trigger appears as an incoming request, with duration, success/failure status, and HTTP response code.
Dependencies — each tool call the agent makes appears as a dependency call, with the target service, duration, and result. Moreover, for an agent loop that invokes Azure OpenAI and Azure AI Search, you will see both as dependency entries, making it straightforward to identify which tool call is slowest.
Exceptions — any workflow failure surfaces as an exception with a full stack trace, correlated to the specific run and iteration where it occurred.
Custom metrics — Logic Apps emits custom metrics for agent loop iterations, token usage, and tool invocation counts. These are queryable via Kusto (KQL) in the Logs blade.
Useful KQL queries for agent loops
You can query agent loop run durations for, let’s say, over the last 72 hours:
requests | where timestamp > ago(72h) | where name contains "agent" | summarize avg(duration), max(duration), count() by bin(timestamp, 1h) | render timechart
To identify failed agent loop runs:
requests | where timestamp > ago(72) | where success == false | project timestamp, name, duration, resultCode, cloud_RoleInstance | order by timestamp desc
To track tool call durations:
dependencies | where timestamp > ago(24h) | where type == "HTTP" | summarize avg(duration), count() by target | order by avg_duration desc
Reading the run history for agent loops
The run history in the Logic Apps portal is the fastest way to debug a specific agent loop run. For agentic workflows it shows more than a conventional run history — each agent action expands to show its iterations, and each iteration shows the model’s reasoning, the tool calls it made, and the results it received.
The Agent activity tab is the most useful view for agentic workflows. It shows the conversation between the model and the tools in chronological order, every message the model generated, every tool it invoked, and every result it received. The agent loop reveals its chain of thought.
Key things to look for in the run history:
Iteration count — how many Think → Act → Observe cycles the loop ran. A loop that runs the maximum number of iterations (default 100) without completing is a signal that the instructions are ambiguous or the tools are not returning usable results.
Tool call inputs and outputs — expand each tool call to see exactly what the model passed as parameters and what the tool returned. This is the fastest way to diagnose a tool that is returning unexpected data.
Token usage — the metadata output of each agent action shows total tokens, prompt tokens, and completion tokens. High prompt token counts indicate the conversation history is growing large — consider enabling agent history reduction.
Azure Logic Apps agent loop production pricing: Standard versus Consumption
The pricing model for agentic workflows differs between Standard and Consumption, and it differs significantly from conventional Logic Apps pricing.
Standard
Standard logic apps use a fixed App Service Plan pricing model — you pay for the compute capacity whether the workflow is running or not. Agentic workflows on Standard do not incur extra charges beyond the base App Service Plan cost. However, every Azure OpenAI call the agent makes is billed separately against your Azure OpenAI resource at standard token rates.
For the la-agent-loop workflows in this series:
The Standard logic app itself: App Service Plan (Workflow Standard WS1 or higher)
Each GPT-4o call: billed to aoai-demo-ptu at your PTU reservation rate
Azure AI Search queries (if used): billed separately at Search tier rates
The practical implication is that Standard agentic workflow costs scale with model usage, not with workflow execution count. A loop that runs five iterations and calls GPT-4o five times costs five times more in model tokens than a loop that resolves in one iteration.
Consumption
Consumption agentic workflows use a pay-as-you-go model. Agent loop pricing is based on the number of tokens each agent action uses and appears as Enterprise Units on your bill. This is a different billing unit from the standard Consumption action executions — each token consumed by the agent is metered separately.
The Consumption agent loop is also subject to throttling based on token usage — unlike Standard, which is constrained only by the App Service Plan compute capacity.
For production workloads with predictable, high-volume agent loop usage, Standard with a PTU Azure OpenAI deployment is the more cost-predictable option. For low-volume or experimental workloads, Consumption pay-as-you-go avoids the fixed App Service Plan cost.
Known limits for agentic workflows
Before going to production, be aware of the current platform limits:
Tool constraints — tools can only contain actions, not triggers. A tool must start with an action and always contains at least one action. Control flow actions (conditions, loops, switches) are not supported inside tools. A tool only works inside the agent loop where it is defined — it cannot be shared across agent actions.
Consumption-specific limits — Consumption agentic workflows can only be created in the Azure portal, not Visual Studio Code. The AI model can come from any region, so data residency for a specific region is not guaranteed for data the model handles. The agent action is throttled based on token usage.
Agent history — by default the agent loop accumulates the full conversation history across iterations. For long-running loops this can push the context length toward the model’s limit. Enable agent history reduction in the agent action’s Settings tab to manage this. The default strategy is token count reduction with a ceiling of 128,000 tokens — adjust this based on your model’s context window and your scenario’s complexity.
Deploying agentic workflows through a DevOps pipeline
Standard logic apps are built on the Azure Functions runtime and deploy the same way as any other Standard logic app — via zip deploy, Azure Pipelines, or GitHub Actions. The workflow definitions are JSON files on disk, making them version-controllable and deployable through standard CI/CD patterns.
What to include in source control
For an agentic workflow project, the key files to version-control are:
sequential-agents/workflow.json — the sequential agent loop definition
sample/workflow.json — the autonomous agent from Post 2
mcp-research/workflow.json — the MCP research workflow from Post 4
connections.json — connection references (without credentials — those go in Key Vault)
host.json — Logic Apps host configuration
local.settings.json — local development settings (excluded from source control, .gitignore)
Deploying with Azure CLI
The simplest production deployment from a CI/CD pipeline uses the Azure CLI:
# Zip the logic app project zip -r la-agent-loop.zip . -x "*.git*" "local.settings.json"
# Deploy to Azure az logicapp deployment source config-zip \ --name la-agent-loop \ --resource-group rg-ai-solutions \ --src la-agent-loop.zip
Environment-specific configuration
Agent connections and app settings differ between development and production environments. Use Azure CLI or Bicep to set environment-specific app settings as part of the deployment pipeline:
az logicapp config appsettings set \ --name la-agent-loop \ --resource-group rg-ai-solutions \ --settings \ agent_openAIEndpoint="https://aoai-prod.openai.azure.com/" \ OPENAI__endpoint="https://aoai-prod.openai.azure.com/"
This keeps environment-specific values out of source control and injected at deploy time — the standard twelve-factor app pattern applied to Logic Apps.
Closing the series
This post closes a seven-part series on Azure Logic Apps agent loop production operations, from first principles through to observability, pricing, and DevOps deployment. The series covered:
Why the agent loop is a different design paradigm from conventional workflow automation
Observability, pricing, and production operations — this post
The agent loop is still a rapidly evolving capability in Azure Logic Apps. The platform limitations documented throughout this series Foundry Models connection persistence, API Center MCP wizard regional constraints, Foundry OpenAPI tool network restrictions will be addressed in future platform releases. The architectural patterns, however, are stable: the four building blocks of an agent loop, the three tooling layers, the four multi-agent patterns, and the two-concern security model will remain the right mental model for this platform regardless of how the surface-level tooling evolves.
Microsoft Foundry Citadel Platform on Azure is a layered AI governance architecture that delivers production-ready agent deployments with unified governance, end-to-end observability, and centralized policy enforcement via Azure API Management. It is still in preview, and the documentation assumes a degree of familiarity with Azure infrastructure that not everyone has on day one. This post walks through what it actually takes to get a working hub-and-spoke running in Sweden Central, including the pitfalls, so you can decide whether it is a viable starting point for your own AI platform journey.
What Citadel Is (and Is Not)
Before touching the tooling, it helps to understand what Citadel actually deploys. The architecture has four layers:
The first layer — Governance Hub is the runtime enforcement plane: Azure API Management as a centralized AI gateway, Azure API Center as a model registry, and supporting services for content safety, PII detection, cost attribution, and usage telemetry.
Subsequent second layer 2 — AI Control Plane provides observability via the Foundry Control Plane: agent-level execution traces, AI evaluations in development and production, red-teaming, drift monitoring, and fleet dashboards.
The next third layer — Agent Identity transforms agents into managed enterprise assets via Microsoft Entra ID, with lifecycle management, sponsorship models for human accountability, and shadow AI discovery.
Finally, the last fourth layer, 4 — Security Fabric, weaves Defender, Purview, and Entra across the other three layers for real-time threat intelligence, data governance, and compliance automation.
For this guide, we deploy Layer 1 (the Governance Hub via the AI Hub Gateway Solution Accelerator) and a Layer 1/2 spoke (via the AI Landing Zone Bicep). Layers 3 and 4 reference existing Azure services (Entra ID, Defender, Purview) that you integrate separately.
Important: Citadel is currently in preview. The repos, parameter schemas, and CLI commands will change. Treat everything in this post as a starting point, not a stable reference.
Prerequisites
Before you start, make sure you have:
An Azure subscription with Azure OpenAI access approved (aka.ms/oaiapply)
Microsoft.Authorization/roleAssignments/write on the subscription (Owner or User Access Administrator role)
Azure CLI installed and authenticated (az login)
Azure Developer CLI (azd) installed
Node.js — use v20 LTS, not v24. Node 24 on Windows has a known issue where npm bundles are incomplete, causing MODULE_NOT_FOUND errors on npm-cli.js and npm-prefix.js when azd tries to package Logic App components
If you run into npm issues on Windows, the cleanest workaround is Azure Cloud Shell, where Node, npm, az, and azd are all pre-installed and healthy.
Part 1: Deploying the Microsoft Foundry Citadel Governance Hub
Create a parameters file at infra/main.parameters.json. The key decisions:
Model versions matter. At the time of writing, gpt-4o-mini versions 2024-07-18 and 2024-10-18 are retired. Use gpt-4o version 2024-11-20 with GlobalStandard SKU. Always verify current model availability at aka.ms/aoai-regions before deploying these changes frequently.
Expect 45–90 minutes. APIM Developer SKU is the slow component. If the deployment fails partway through, re-run azd up it is idempotent and will pick up where it left off.
The AI Hub Gateway Solution Accelerator was deployed successfully in Azure Sweden Central after 21 hours and31 minutes, provisioning APIM, Azure OpenAI, Content Safety, Application Insights, private endpoints, and the usage processing Logic App.
Pitfall: Managed Identity Race Condition
You will likely see this error on first attempt:
BadRequest: The provided principal ID was not found in the AAD tenant(s)
This is a known race condition — the Managed Identity is created but has not yet propagated in Entra ID before the role assignment fires. Re-run azd up without any changes and it will succeed.
Validate the Hub
Once deployed, run:
azd env get-values | grep APIM
You will get your APIM gateway URL. Test it with a chat completion:
$headers=@{
"Content-Type"="application/json"
"api-key"="<YOUR_APIM_SUBSCRIPTION_KEY>"
}
$body='{"messages":[{"role":"user","content":"Hello from the AI Hub Gateway!"}],"max_tokens":100}'
Validating the Citadel Governance Hub by calling the APIM gateway endpoint via PowerShell, the response confirms gpt-4o-2024-11-20 routing, Content Safety filtering, PII redaction, and token usage tracking are all active.
A successful response with content_filter_results and prompt_filter_results confirms Content Safety and PII redaction are active. Token usage in the response confirms Cosmos DB is logging for cost attribution.
Part 2: Deploying a Citadel Platform Agent Spoke on Azure
The spoke is deployed from the AI Landing Zone Bicep repo. Download it as a ZIP (no GitHub account required):
Extract and navigate to the folder. Create a resource group for the spoke:
az group create --name rg-ai-spoke-dev --location swedencentral
Create a spoke.parameters.json file. Several things to know upfront:
The parameter schema is not the same as the Citadel README suggests. The actual template parameters differ from the example file. Key differences discovered in practice: aiFoundryLocation does not exist as a separate parameter; deployMcp, greenFieldDeployment, deployPostgres, and useCMK are not in this version of the template; and solutionStorageAccountName is simply storageAccountName.
The modelDeploymentList uses nested objects, not flat properties:
containerAppsList cannot be an empty array. The template references containerApps[0] internally and will fail validation if the array is empty. Pass at least one placeholder entry.
Deploy:
az deployment group create`
--resource-group rg-ai-spoke-dev `
--template-file main.bicep`
--parameters @spoke.parameters.json
Pitfalls in the Spoke Deployment
AI Search Standard SKU capacity exhaustion. Sweden Central frequently runs out of AI Search Standard SKU capacity. You will see ResourcesForSkuUnavailable. This affects both the standalone Search Service and the AI Foundry Agent Service’s internal Search instance. Disable both:
"deploySearchService": { "value": false },
"deployAAfAgentSvc": { "value": false }
You can re-enable them later once capacity is available, or deploy Search in a different region.
Soft-deleted resources block redeployment. Azure retains soft-deleted Cognitive Services accounts, Key Vaults, and App Configuration stores for up to 90 days. If you delete a resource group and redeploy, the deployment will fail with FlagMustBeSetForRestore or NameUnavailable. Purge them explicitly before redeploying:
# List and purge soft-deleted resources
az keyvault list-deleted --subscription <sub-id> -o table
az keyvault purge --name <name> --location swedencentral
az appconfig list-deleted --subscription <sub-id> -o table
az appconfig purge --name <name> --location swedencentral --yes
az cognitiveservices account list-deleted --subscription <sub-id> -o table
az cognitiveservices account purge --name <name> --location swedencentral
Key Vault purges are slow — allow 2–5 minutes per vault.
Bastion subnet ID resolution fails with networkIsolation=false. When you disable network isolation, the template passes a relative subnet ID to Bastion instead of a fully qualified resource ID. Disable Bastion, Jump VM, and NAT Gateway for the dev spoke:
"deployBastion": { "value": false },
"deployJumpbox": { "value": false },
"deployVM": { "value": false },
"deployNatGateway": { "value": false }
Write parameters files without BOM. On Windows, Out-File -Encoding utf8 adds a Byte Order Mark that causes az deployment to fail with Unable to parse parameter. Use either:
Note: az cognitiveservices account connection create with a YAML file for creating an APIM connection in AI Foundry has known bugs in the current CLI version and will throw NoneType or codec errors. Create this connection via the Azure AI Foundry portal UI instead.
Validate End-to-End
$headers = @{
"Content-Type" = "application/json"
"api-key" = "<YOUR_APIM_KEY>"
}
$body = '{"messages":[{"role":"user","content":"Hello from the Citadel spoke!"}],"max_tokens":50}'
A successful response with content_filter_results, prompt_filter_results, and usage confirms the full Citadel loop: spoke → APIM gateway → Azure OpenAI → governance telemetry.
End-to-end validation of the Citadel hub-and-spoke setup: a request from the agent spoke routes through the APIM Governance Hub in Sweden Central, returning a successful gpt-4o response, with Content Safety filtering and token usage tracking confirmed.
What the Microsoft Foundry Citadel Platform Deploys
After following this guide, your rg-ai-hub-gateway-dev resource group contains:
APIM gateway with content safety, PII redaction, token rate limiting, and cost attribution policies
Azure OpenAI with gpt-4o and text-embedding-3-large
App Configuration is fully populated with canonical keys (CHAT_DEPLOYMENT_NAME, AI_FOUNDRY_PROJECT_ENDPOINT, COSMOS_DB_ENDPOINT, and more) ready for agent applications to consume.
This Is a Dev Setup — Here Is What Changes for Non-Prod and Production
The configuration above is a starting point, not a production blueprint. Key differences when moving up the environment stack:
APIM SKU. Developer SKU has no SLA and no VNet support. Switch to Premium SKU for non-prod and production. This significantly increases cost and deployment time but enables private networking, multi-region, and availability zones.
Network isolation. For production, set networkIsolation=true and wire the spoke VNet to your hub VNet via peering (hubIntegrationHubVnetResourceId). This requires coordinating private DNS zones across the hub and spoke. The template supports bringing existing DNS zones via the existingPrivateDnsZone* parameters.
AI Search. Re-enable deploySearchService and deployAAfAgentSvc for non-prod and production. If Sweden Central remains capacity-constrained on Standard SKU, deploy Search to a paired region (East US 2 works well) using the searchServiceLocation parameter.
Bastion and Jump VM. For production with networkIsolation=true, re-enable deployBastion and deployJumpbox so operators can access resources inside the private VNet without public endpoints.
Separate parameter files per environment. Maintain spoke.parameters.dev.json, spoke.parameters.nonprod.json, and spoke.parameters.prod.json with environment-specific values. Use a deployment pipeline (GitHub Actions or Azure DevOps) to apply them consistently.
Model versions. Pin specific model versions in parameters files and validate availability in your target region before each deployment. Azure OpenAI model lifecycle moves fast; versions retire on 18-month cycles, and regional availability varies.
Preview Caveats
Citadel is in active development. Several things you should expect to change:
The parameter schemas for both the hub and spoke accelerators will evolve. Parameters discovered missing or renamed in this guide will likely be reorganized again as the repos mature. Always check the actual main.bicep parameter definitions rather than relying on example files.
The az cognitiveservices account connection create CLI command for AI Foundry connections is incomplete at the time of writing. This will improve as the Foundry CLI surface area matures.
The citadel-v1 branch in the AI Hub Gateway repo is flagged as the recommended path for new deployments. By the time you read this, it may have become the default branch with a cleaner deployment experience.
Regional capacity for AI Search Standard SKU fluctuates. Sweden Central is a high-demand region for AI workloads plan for capacity constraints in any SKU beyond Basic for dev scenarios.
Conclusion
Citadel gives you a credible, opinionated starting point for enterprise AI governance on Azure APIM as the AI gateway, AI Foundry as the agent runtime, Cosmos DB for conversation state, and App Configuration as the configuration backbone. Getting it running today requires navigating several rough edges: parameter schema inconsistencies, soft-delete cascades, model version deprecations, regional capacity constraints, and Windows-specific tooling issues.
None of these are blockers. They are the expected friction of working with a platform in active preview. The underlying architecture is sound, and the pieces that do work, APIM governance policies, Content Safety integration, App Config population, and AI Foundry project wiring deliver real value immediately.
If you are building an AI platform for your organization, a Citadel dev setup is a reasonable first step. Treat it as a learning environment to understand the architecture, validate the tooling, and build the parameter files you will need for non-prod and production. Then evolve it deliberately: add network isolation, re-enable Search and Agent Services as capacity allows, and adopt the Citadel contracts (AI Access Contract, AI Publish Contract) to formalize the hub-spoke integration as your agent portfolio grows.
The governance-velocity paradox Citadel sets out to solve is real. Getting the foundation right now, while it is still in preview and the patterns are malleable, is the right time to start.
Final note: This post reflects a hands-on deployment performed in June 2026. Given the pace of change in this space, verify all CLI commands, parameter schemas, and model versions against current documentation before applying them in your own environment.
Azure API Management as MCP gateway is the natural endpoint of everything this series has built. In Parts 1 through 6, we established APIM as the control plane for AI workloads: securing access, limiting and measuring token consumption, routing traffic resiliently across backends, and reducing costs through semantic caching. All of that applies equally to agentic workloads. The difference is that agents introduce a new communication pattern: the Model Context Protocol (MCP), which standardizes how AI agents discover and call tools.
In my work and online research on agentic AI architecture, I consistently returned to the same question: how does one govern agent tool calls with the same rigor we apply to API calls? The answer, increasingly, is that APIM handles both. This post covers what that looks like in practice.
What MCP Is and Why It Changes the APIM Story
MCP is an open protocol, originally developed by Anthropic, that defines a standard interface between AI agents (MCP clients) and the tools they call (MCP servers). Instead of each agent framework implementing its own bespoke tool-calling mechanism, MCP gives agents a consistent way to discover available tools, understand their input schemas, and invoke them. Frameworks including Semantic Kernel, AutoGen, and LangGraph are all adding MCP client support.
For APIM, MCP matters because it transforms the gateway from a proxy for AI completions into a broker for agent tool calls. An agent no longer calls your internal APIs directly. Instead, it discovers them as MCP tools through APIM, and APIM enforces the same governance policies on those tool calls that it enforces on any other request. The control plane extends naturally into the agentic layer.
Azure API Management as MCP Gateway: Three Capabilities
Azure API Management as an MCP gateway. Existing REST APIs are auto-exposed as MCP tool definitions via the export-rest-mcp-server policy. External MCP servers are proxied through APIM. Agent-to-agent traffic passes through the same inbound policy pipeline, with all series policies, authentication, token limits, token metrics, andload balancing applied uniformly.
APIM’s MCP gateway capabilities fall into three categories:
Expose REST APIs as MCP servers. The export-rest-mcp-server policy takes any API already registered in your APIM catalog and auto-generates MCP tool definitions from it. An agent connecting to your APIM MCP endpoint discovers those tools via the standard MCP protocol and can call them without any knowledge of the underlying REST implementation. Crucially, no changes are required to the underlying API. The policy handles the translation layer entirely within APIM.
Pass through external MCP servers. APIM can proxy external MCP servers — whether third-party services like GitHub or Jira, or custom MCP servers built by your own teams — through the same gateway. All traffic passes through APIM’s policy pipeline, so you apply JWT validation, subscription key enforcement, token limits, and logging to external MCP calls exactly as you would to any other API call. Agents get a single APIM endpoint; APIM handles the routing.
Agent-to-agent (A2A) traffic. In multi-agent architectures, orchestrator agents call sub-agents to delegate tasks. Routing that traffic through APIM means every A2A hop is governed: authenticated, rate-limited, logged, and subject to the same token budget controls applied to end-user traffic. This is particularly relevant for agentic pipelines running on Microsoft Foundry, where multiple specialized agents collaborate within a single workflow.
Applying Series Policies to Agentic Workloads
One of the practical advantages of routing MCP traffic through APIM is that every policy covered in this series applies without modification. Agentic workloads are not a special case requiring a separate governance layer. They use the same pipeline.
Authentication (Part 2): Agents authenticate to APIM using subscription keys or JWT tokens. APIM authenticates to AI backends via Managed Identity. The agent never holds backend credentials.
Token limits (Part 3): Multi-step agentic pipelines can consume large token volumes per workflow. Per-subscription TPM limits prevent a single runaway pipeline from exhausting shared capacity.
Token metrics (Part 4): Token consumption from agentic workflows is attributed to the subscribing team or pipeline via the emit-token-metric policy. FinOps visibility extends automatically to agentic workloads.
Load balancing (Part 5): Agentic pipelines often run longer and consume more tokens per call than chat applications. PTU-to-PAYG failover protects pipeline continuity when primary capacity saturates.
Semantic caching (Part 6): Agents that make repeated identical tool calls, checking a status, or looking up a reference value, benefit from semantic caching in the same way chat applications do.
Practical Considerations for APIM as MCP Gateway
A few agentic-specific considerations are worth calling out before you start routing MCP traffic through APIM.
Tool discovery latency. MCP clients typically discover available tools at session start by calling the MCP server’s tool list endpoint. With APIM in the path, that discovery call passes through the full policy pipeline. Keep your inbound policies lightweight for discovery calls, or cache the tool list response to avoid repeated round trips.
Streaming responses. Many AI completions endpoints support streaming via server-sent events. APIM supports streaming passthrough, but some policies — including semantic cache lookup — do not apply to streaming responses. Structure your pipeline accordingly: apply caching only to non-streaming completion calls.
Session state. MCP conversations are stateful within a session. APIM is stateless between requests, so per-session state must live in the calling agent or an external store. The vary-by pattern from the semantic cache policy can scope cached tool responses by session ID if the agent passes one in a header.
Token budget propagation. In multi-agent pipelines, token budgets need to propagate from the orchestrator to sub-agents. Exposing the remaining token budget from the remaining-tokens-variable-name attribute (Part 3) as a response header lets orchestration frameworks like Semantic Kernel make informed decisions about which sub-agent to invoke next.
Azure API Management as MCP Gateway: Closing the Series
This post closes the series, but the control plane it describes is not static. MCP is still evolving rapidly. New APIM policy capabilities for agentic workloads are shipping frequently. The architecture board conversation at various enterprise has shifted from “should we centralize AI traffic through APIM?” to “what do we govern next?”, which is a good place to be.
The complete APIM for AI control plane across all seven parts of the series. One APIM instance governs every consumer type, every Azure AI backend, and every governance requirement — including agentic MCP workloads introduced in this post. Each policy layer can be implemented incrementally, starting with authentication and adding capability as workloads mature.
Looking back across the seven posts, the consistent theme is that AI workloads are not fundamentally different from other API workloads in terms of governance requirements. They need authentication, rate limiting, observability, resilience, and cost control. APIM provides all of those. What changes with AI is the unit of measurement (tokens, not requests), the billing model (PTU vs. PAYG), and now the communication protocol (MCP for agents). The control plane adapts to each of these without requiring a parallel governance infrastructure.
The full series index is below for reference. Each post links to the relevant Microsoft documentation and includes policy XML you can use directly.
Earlier, AWS officially launched its European Sovereign Cloud, backed by a €7.8 billion investment in Brandenburg, Germany. The infrastructure is physically and logically separated from AWS global regions, managed by a new German parent company (AWS European Sovereign Cloud GmbH), and staffed exclusively by EU residents. On paper, it checks every compliance box for data residency and operational sovereignty. AWS CEO Matt Garman called it “a big bet” for the company, and it is. The question is whether it’s the right bet for Europe.
European Sovereign Cloud: Real Isolation, Real Trade-offs
The technical separation is genuine. An AWS engineer who deployed services to the European Sovereign Cloud confirmed on Hacker News that proper boundaries exist—U.S.-based engineers can’t see anything happening in the sovereign cloud. To fix issues there, they play “telephone” with EU-based engineers. The infrastructure uses the partition name *aws-eusc* and the region name *eusc-de-east-1*, which are completely separate from AWS’s global regions. All components, IAM, billing systems, and Route 53 name servers using European Top-Level Domains—remain within EU borders.
But this isolation comes with costs. As that same engineer warned, “it really slows down debugging issues. Problems that would be fixed in a day or two can take a month.” This is the sovereignty trade-off in practice: more control, less velocity. The service launches with approximately 90 AWS services, not the full catalog. Plans exist to expand into sovereign Local Zones in Belgium, the Netherlands, and Portugal, but this remains a subset of AWS’s offerings globally.
For some workloads, this trade-off makes sense. For others, it’s a deal-breaker.
Why the European Sovereign Cloud Can’t Escape U.S. Jurisdiction
Here’s the uncomfortable truth that AWS’s marketing carefully sidesteps: technical isolation doesn’t create legal isolation. AWS, headquartered in America, remains subject to U.S. jurisdiction. The CLOUD Act allows U.S. authorities to compel U.S.-based technology companies to provide data, regardless of where it is stored globally. Courts can require parent companies to produce data held by subsidiaries.
This isn’t theoretical hand-wraving. Microsoft had to admit in a French court that it cannot guarantee data sovereignty for EU customers. When Airbus executive Catherine Jestin discussed AWS’s sovereignty claims with lawyers late last year, she said: “I still don’t understand how it is possible” for AWS to be immune to extraterritorial laws.
Cristina Caffarra, founder of the Eurostack Foundation and competition economist, puts it bluntly:
A company subject to the extraterritorial laws of the United States cannot be considered sovereign for Europe. That simply doesn’t work.
The AWS response focuses on technical controls—encryption, the Nitro System preventing employee access, and hardware security modules. These are important safeguards, but they don’t address the core legal issue. If a U.S. court orders Amazon.com Inc. to produce data, technical barriers become legal obstacles the parent company must overcome, not protections.
Europe’s European Sovereign Cloud Strategy: The Cloud and AI Development Act
AWS’s launch comes as Europe finalizes its own legislative response. The EU Cloud and AI Development Act, expected in Q1 2026, aims to strengthen Europe’s autonomy over cloud infrastructure and data. As Christoph Strnadl, CTO of Gaia-X, explains:
For critical data, you will never, ever use a US company. Sovereignty means having strategic options — not doing everything yourself.
The Act is part of the EU’s Competitiveness Compass and addresses a fundamental problem: Europe’s 90% dependency on non-EU cloud infrastructure, predominantly American companies. This dependency isn’t just about data residency—it’s about strategic autonomy. When essential services depend on infrastructure governed by foreign law, questions arise about jurisdiction, resilience, and what happens during geopolitical disruption.
Current estimates indicate that AWS, Microsoft Azure, and Google Cloud collectively control over 60% of the European cloud market. European providers account for only a small share of revenues. The Cloud and AI Development Act aims to establish minimum criteria for cloud services in Europe, mobilize public and private initiatives for AI infrastructure, and create a single EU-wide cloud policy for public administrations and procurement.
Importantly, Brussels isn’t seeking to ban non-EU providers. As Strnadl notes:
Sovereignty does not mean you have to do everything yourself. Sovereignty means that for critical things, you have strategic options.
Gaia-X and the European Sovereign Cloud: A Lesson in Sovereignty Washing
Europe has been down this path before. Gaia-X, launched in 2019, intended to create a trustworthy European data infrastructure. Then American companies lobbied to be included. Once Microsoft, Google, and AWS were inside, critics argue, Gaia-X lost its purpose. The fear now is that AWS’s European Sovereign Cloud represents sophisticated “sovereignty washing”—placing datacenters on European soil without resolving the fundamental legal issue.
Recent European actions suggest growing awareness of this problem. Austria, Germany, France, and the International Criminal Court in The Hague are taking concrete steps toward genuine digital independence. These aren’t just policy statements—they’re actual migrations away from U.S. hyperscalers toward European alternatives.
European Sovereign Cloud Adoption: No Full Migration in 2026
Forrester predicts that no European enterprise will fully shift away from U.S. hyperscalers in 2026, citing geopolitical tensions, volatility, and new legislation, such as the EU AI Act, as barriers. The scale of dependency is too deep, the feature gap too wide, and the migration costs too high for rapid change.
Gartner forecasts European IT spending will grow 11% in 2026 to $1.4 trillion, with 61% of European CIOs and tech leaders wanting to increase their use of local cloud providers. Around half (53%) said geopolitical factors would limit their use of global providers in the future. The direction is clear, even if the pace remains uncertain.
This creates a transitional period where organizations must make pragmatic choices. For non-critical workloads, AWS’s European Sovereign Cloud may be sufficient. For truly sensitive data—government communications, defense systems, critical infrastructure—organizations need genuinely European alternatives: Hetzner, Scaleway, OVHCloud, StackIT by Schwarz Digits.
What AWS’s European Sovereign Cloud Actually Delivers
Let’s be precise about what AWS European Sovereign Cloud achieves. It provides:
Data residency within the EU
Operational control by EU residents
Governance through EU-based legal entities
Technical isolation from the global AWS infrastructure
An advisory board of EU citizens with independent oversight
What it doesn’t provide is independence from U.S. legal jurisdiction. For compliance requirements focused purely on data residency and operational transparency, this may be sufficient. For organizations requiring protection from U.S. government data requests, it fundamentally isn’t.
As Eric Swanson from CarMax noted in a LinkedIn post:
Sovereign cloud offerings do not override the Patriot Act. They mainly reduce overlap across other contexts: data location, operational control, employee access, and customer jurisdiction.
European Sovereign Cloud and Strategic Autonomy: Not Autarky
Europe’s path forward isn’t about digital isolationism. As Strnadl emphasizes, technology adoption that involves a paradigm shift doesn’t happen in two years. The challenge is adoption, not frameworks. “Cooperation needs trust,” he says, “and trust needs a trust framework.”
The Cloud and AI Development Act, expected this quarter, will provide that framework. It will set minimum criteria, promote interoperability, and establish procurement rules that favor sovereignty for critical workloads. The question for organizations is: what constitutes critical?
For email, public administration, political communication, and defense systems, the answer should be obvious. These require European alternatives. For other workloads, AWS’s European Sovereign Cloud may strike an acceptable balance between capability and control.
The Bottom Line
AWS’s €7.8 billion investment is real. The technical isolation is real. The economic contribution to Germany’s GDP (€17.2 billion over 20 years) is real. What’s also real is that Amazon.com Inc., a U.S. company, ultimately controls this infrastructure and remains subject to U.S. law.
For organizations seeking compliance checkboxes and data residency guarantees, AWS European Sovereign Cloud delivers. For organizations requiring genuine independence from U.S. legal jurisdiction, it remains fundamentally insufficient. That’s not a criticism of AWS’s engineering—it’s a statement of legal reality.
The sovereignty question Europe faces isn’t technical. It’s strategic: do we accept managed dependency or build genuine autonomy? AWS offers the former. Only European alternatives can provide the latter.
Europe’s sovereignty challenge has moved from political debate to concrete policy. With the EU’s new Cloud Sovereignty Framework now in place, the continent is redefining how it procures and governs cloud infrastructure, shifting from dependency on foreign providers to measurable, auditable control over its digital destiny.
Today, Europe and the Netherlands find themselves at a crucial junction, navigating the complex landscape of digital autonomy. The recent introduction of the EU’s new Cloud Sovereignty Framework is the clearest signal yet that the continent is ready to take back control of its digital destiny.
This isn’t just about setting principles; it’s about introducing a standardized, measurable scorecard that will fundamentally redefine cloud procurement.
Europe’s Sovereignty Challenge: Why Digital Independence Is Non-Negotiable
The digital revolution has brought immense benefits, yet it has also positioned Europe in a state of significant dependency. Approximately 80% of our digital infrastructure relies on foreign companies, primarily American cloud providers. This dependence is not merely a matter of convenience; it’s a profound strategic vulnerability.
The core threat stems from U.S. legislation such as the CLOUD Act, which grants American law enforcement the power to request data from U.S. cloud service providers, even if that data is stored abroad. Moreover, this directly clashes with Europe’s stringent privacy regulations (GDPR) and exposes critical European data to external legal and geopolitical risk.
As we’ve seen with incidents like the Microsoft-ICC blockade, foreign political pressures can impact essential digital services. The possibility of geopolitical shifts, such as a “Trump II” presidency, only amplifies this collective awareness: we cannot afford to depend on foreign legislation for our critical infrastructure. The risk is present, and we must build resilience against it.
The Sovereignty Scorecard: From Principles to SEAL Rankings
The new Cloud Sovereignty Framework is the EU’s proactive response. It shifts the discussion from abstract aspirations to concrete, auditable metrics by evaluating cloud services against eight Sovereignty Objectives (SOVs) that cover legal, strategic, supply chain, and technological aspects.
The result is a rigorous “scorecard.” A provider’s weighted score determines its SEAL ranking (from SEAL-0 to SEAL-4, with SEAL-4 indicating full digital sovereignty). Crucially, this ranking is intended to serve as the definitive minimum assurance factor in government and public sector cloud procurement tenders. The Commission wants to create a level playing field where providers must tangibly demonstrate their sovereignty strengths.
Hyperscalers vs. European Providers: The Cloud Sovereignty Challenge
The framework has accelerated a critical duality in the market: massive, centralized investments by US hyperscalers versus strategic, federated growth by European alternatives.
Hyperscalers Adapt: Deepening European Ties
Global providers are making sovereignty a mandatory architectural and legal prerequisite by localizing their operations and governance.
AWS explicitly responded by announcing its EU Sovereign Cloud unit. This service is structured to ensure data residency and operational autonomy within Europe, explicitly targeting the SOV-3 (Data & AI Sovereignty: The degree of control customers have over their data and AI models, including where data is processed) criteria through physically and logically separated infrastructure and governance.
Google Cloudhas also made significant moves, approaching digital sovereignty across three distinct pillars:
Data Sovereignty (focusing on control over data storage, processing, and access with features like the Data Boundary and External Key Management, EKM, where keys can be held outside Google Cloud’s infrastructure);
Operational Sovereignty (ensuring local partner oversight, such as the partnership with T-Systems in Germany); and
Software Sovereignty(providing tools to reduce lock-in and enable workload portability).To help organizations navigate these complex choices, Google introduced theDigital Sovereignty Explorer, an interactive online tool that clarifies terms, explains trade-offs, and guides European organizations in developing a tailored cloud strategy across these three domains. Furthermore, Google has developed highly specialized options, includingAir-Gappedsolutions for the defense and intelligence sectors, demonstrating a commitment to the highest levels of security and residency.
Microsoft has demonstrated a profound deepening of its commitment, outlining five comprehensive digital commitments designed to address sovereignty concerns:
Massive Infrastructure Investment: Pledging a 40% increase in European datacenter capacity, doubling its footprint by 2027.
Governance and Resilience: Instituting a “European cloud for Europe” overseen by a dedicated European board of directors (composed exclusively of European nationals) and backed by a “Digital Resilience Commitment” to contest any government order to suspend European operations legally.
Data Control: Completing the EU Data Boundary project to ensure European customers can store and process core cloud service data within the EU/EFTA.
European Contenders Scale Up
Strategic, open-source European initiatives powerfully mirror this regulatory push:
Virt8ra Expands: The Virt8ra sovereign cloud, which positions itself as a significant European alternative, recently announced a substantial expansion of its federated infrastructure. The platform, coordinated by OpenNebula Systems, added six new cloud service providers, including OVHcloud and Scaleway, significantly broadening its reach and capacity across the continent.
IPCEI Funding: This initiative, leveraging the open-source OpenNebula technology, is part of the Important Project of Common European Interest (IPCEI) on Next Generation Cloud Infrastructure and Services, backed by over €3 billion in public and private funding. This is a clear indicator that the vision for a robust, distributed European cloud ecosystem is gaining significant traction.
Redefining European Cloud Sovereignty: Resilience Over Isolation
Industry experts emphasize that the framework embodies a more mature understanding of digital sovereignty. It’s not about isolation (autarky), but about resilience and governance.
Sovereignty is about how an organization is “resilient against specific scenarios.” True sovereignty, in this view, lies in the proven, auditable ability to govern your own digital estate. For developers, this means separating cloud-specific infrastructure code from core business logic to maximize portability, allowing the use of necessary hyper-scale features while preserving architectural flexibility.
The Challenge: Balancing Features with Control
Despite the massive investments and public commitments from all major players, the framework faces two key hurdles:
The Feature Gap: European providers often lack the “huge software suite” and “deep feature integration” of US hyperscalers, which can slow down rapid development. Advanced analytics platforms, serverless computing, and tightly integrated security services often lack direct equivalents at smaller providers. This creates a complex chicken-and-egg problem: large enterprises won’t migrate to European providers because they lack features, but local providers struggle to develop those capabilities without enterprise revenue.
Skepticism and Compliance Complexity: Some analysts fear the framework’s complexity will inadvertently favor the global giants with larger compliance teams. Furthermore, deep-seated apprehension in the community remains, with some expressing the fundamental desire for purely European technological solutions: “I don’t want a Microsoft cloud or AI solutions in Europe. I want European ones.” Some experts suggest that European providers should focus on building something different by innovating with European privacy and control values baked in, rather than trying to catch up with US providers’ feature sets.
My perspective on this situation is that achieving true digital sovereignty for Europe is a complex and multifaceted endeavor. While the commitments from global hyperscalers are significant, the underlying desire for independent, European-led solutions remains strong. It’s about strategic autonomy, ensuring that we, as Europeans, maintain ultimate control over our digital destiny and critical data, irrespective of where the technology originates.
The race is now on. The challenge for the cloud industry is to translate the high-level, technical criteria of the SOVs into auditable, real-world reality to achieve that elusive top SEAL-4 ranking. The battle for the future of Europe’s cloud is officially underway.