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

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

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

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

That’s the problem this post solves.

Why register agents at all

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

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

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

Azure API Center 101

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

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

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

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

The AI Publish Contract pattern

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

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

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

json

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

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

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

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

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

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

Prerequisites

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

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

Step 1: define the custom metadata schema

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

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

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

Step 2: register the agent as an API

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

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

Step 3: create the API version

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

Step 4: attach the definition

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

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

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

That failed immediately:

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

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

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

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

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

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

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

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

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

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

Step 5: register the environment and deployment

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

The portal equivalent FOR Azure API Center

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

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

What it looks like in the fleet dashboard

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

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

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

Connecting to EU AI Act Article 49

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

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

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

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

Pitfalls

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

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

Other Pitfalls

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

What’s next

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

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

Wrapping up the series

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

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

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

Leave a Reply