Backend integration
Connect your Azure AI Foundry project
Connect a Foundry project once, list and inspect the agents already in it, invoke or link them, import one as a draft agent, or publish a published agent back onto Foundry Agent Service — the Entra role, request and response shapes, error codes and the honest status of each capability.
A Foundry project connection is how a tenant works with everything its team already built in Azure AI Foundry Agent Service without moving model traffic off Azure. It is project-level (one connection, many agents) and complements the single-agent external agent registration on Connect an external agent — it does not replace it. Positioning: Agentic Workforce ME is the workforce control plane (hierarchy, manifests, human approvals, tenant audit); Foundry stays the Azure-native runtime plane (Entra, Purview, Defender, Monitor). Link = coexist on Azure; import = run on Agentic Workforce ME instead.
1. The four capabilities and their status
| Capability | Routes | Status |
|---|---|---|
| Understand the agents already in Foundry | GET …/agents (paged inventory on GA /agents, classic /assistants fallback), GET …/agents/:name (definition, tools, knowledge refs, version), …/threads, …/runs (classic surface only) | Verified live on 29 Sep 2026 against a Foundry project |
| Integrate — use the agent where it is | POST …/invoke natively on both API generations (classic threads/runs poll, or POST {project}/openai/v1/responses with agent_reference), audited and metered; POST …/link registers it in external_agents as an A2A peer | Verified live on 29 Sep 2026 (agent-endpoint Responses route, no Foundry-Features header needed; the project-scoped fallback also answers 200) |
| Run Agentic Workforce ME agents on Foundry Agent Service | POST /v1/foundry/connections/:id/publish projects a published manifest to a Foundry prompt agent — admin-only, audited, dry_run, refuses drafts | Verified live once on 29 Sep 2026 against a Foundry project (prompt agents only; hosted/container agents not supported) |
| Migrate Foundry → Agentic Workforce ME (the “Foundry Local replacement”) | POST …/import (live) and POST /v1/foundry/import-definition (offline) produce a valid draft manifest with hitl.default = review and a fidelity report of unmapped tools | Verified live on 29 Sep 2026. Vector stores, Toolbox → MCP, evals and workflow graphs are referenced, not copied (M3 roadmap) |
2. Quickstart — connect your Foundry project
On the Azure side
Foundry Agent Service needs a kind: AIServices account plus a project, and an Entra app registration that holds the Foundry User role on the project. The platform lists and creates agents, so Foundry Agent Consumer (the invoke-only role that is enough for an external-agent A2A peer) is not enough here. Token scope is https://ai.azure.com/.default.
# 1. A Foundry (kind AIServices) account + project. An Azure OpenAI account is not the same thing.
az cognitiveservices account create -n <account> -g <rg> -l <region> \
--kind AIServices --sku S0 --custom-domain <account> \
--assign-identity --allow-project-management true --yes
az cognitiveservices account project create \
--name <account> --resource-group <rg> --project-name <project> --location <region>
# 2. An Entra app registration for the platform + the "Foundry User" role on the project.
# (Microsoft renamed "Azure AI User" to "Foundry User" in 2026 — same role id, either label is the same grant.)
az ad app create --display-name awf-foundry-connector # note appId, then create a client secret
az role assignment create --assignee <appId> \
--role 53ca6127-db72-4b80-b1b0-d745d6d5456d \
--scope $(az cognitiveservices account show -n <account> -g <rg> --query id -o tsv)
# Project endpoint — used verbatim as project_endpoint:
# https://<account>.services.ai.azure.com/api/projects/<project>Connect, then prove reachability
curl -sS -X POST "https://<api-host>/v1/foundry/connections" \
-H "Authorization: Bearer hive_…" -H "X-Tenant-Id: <tenant id>" \
-H "Content-Type: application/json" \
-d '{
"slug": "gov-project",
"name": "Foundry — gov (uaenorth)",
"project_endpoint": "https://<account>.services.ai.azure.com/api/projects/<project>",
"residency_region": "uaenorth",
"auth_kind": "entra_client_credentials",
"credentials": {
"tenant_id": "<entra tenant id>",
"client_id": "<app registration client id>",
"client_secret": "<client secret>"
}
}'
# → 201 { connection: { id, slug, …, auth_kind, credential_meta: { tenant_id, client_id } } }
# credential_meta never carries the secret; no GET route returns it either.# Token + one inventory call; persists last_inventory and the detected surface.
curl -sS -X POST "https://<api-host>/v1/foundry/connections/<id>/test" \
-H "Authorization: Bearer hive_…" -H "X-Tenant-Id: <tenant id>"
# → { ok: true, surface: "agents" | "assistants", count: 2 }
# 401 FOUNDRY_AUTH the Entra token was refused (wrong secret, missing role)
# 502 FOUNDRY_UNREACHABLE the project did not answerproject_endpoint must be https:// and its host must end in .services.ai.azure.com — anything else is 400 VALIDATION before a single byte leaves the platform (the tenant’s Entra bearer is sent to that origin). The credential bag is AES-256-GCM at rest and decrypted only inside the gateway, per call; it is never serialized into prompts, run state, checkpoints, audit rows or logs. PATCH with a new credentials bag rotates it (audited as rotated: true); enabled: false makes every live route answer 409 FOUNDRY_UNREACHABLE.
3. Use it
Understand — inventory and inspect
# Understand: the agents already in the project (GA /agents, classic /assistants fallback; pages of 100, up to 10)
curl -sS "https://<api-host>/v1/foundry/connections/<id>/agents" \
-H "Authorization: Bearer hive_…" -H "X-Tenant-Id: <tenant id>"
# → { surface, items: FoundryAgentView[] }
# Inspect one: definition, tools, knowledge references, version
curl -sS "https://<api-host>/v1/foundry/connections/<id>/agents/<name>" \
-H "Authorization: Bearer hive_…" -H "X-Tenant-Id: <tenant id>"
# → { agent: FoundryAgentView }
# Classic surface only — a new-agents-only project answers 409 FOUNDRY_SURFACE_UNSUPPORTED
curl -sS "https://<api-host>/v1/foundry/connections/<id>/agents/<name>/threads" …
curl -sS "https://<api-host>/v1/foundry/connections/<id>/agents/<name>/runs?thread_id=<thread>" …Integrate — invoke natively, or link as a peer
# Integrate (a): invoke the Foundry agent natively — the run executes in Foundry, on their identity and traces
curl -sS -X POST "https://<api-host>/v1/foundry/connections/<id>/agents/<name>/invoke" \
-H "Authorization: Bearer hive_…" -H "X-Tenant-Id: <tenant id>" \
-H "Content-Type: application/json" \
-d '{ "input": "Screen bid 6003 against bid 6001 for duplicate banking details." }'
# → { result: { text, status: "completed", surface: "agents", remote_response_id, model, usage? } }
# audit_log foundry.invoke + usage_events external_agent_call (ids, status, latency, token counts — never the text)# Integrate (b): register the same agent as an external agent (A2A peer) so your agents can call it as a tool / step / delegate
curl -sS -X POST "https://<api-host>/v1/foundry/connections/<id>/agents/<name>/link" \
-H "Authorization: Bearer hive_…" -H "X-Tenant-Id: <tenant id>" \
-H "Content-Type: application/json" \
-d '{ "trust_level": "partner" }'
# → 201 { external_agent: { id, slug, name } } (provider foundry, protocol a2a-1.0, same encrypted bag)
# 409 FOUNDRY_SURFACE_UNSUPPORTED on an api_key connection — the A2A peer adapter is Entra-only/invoke is an operator / test path: the raw text comes back to the admin and the run executed in Foundry — its trace lands in their Application Insights; the platform records an audit row and a usage row with the remote ids (correlation, not a second copy of Azure Monitor). Once linked, the Foundry agent is one external agent among the others — tool grants, approval policies, the trust floor and the untrusted-output wrapper all apply, exactly as on Connect an external agent.
Migrate in — import to a draft
# Migrate in (a): from the live project → a DRAFT agent under node_id, HITL defaulted to "review"
curl -sS -X POST "https://<api-host>/v1/foundry/connections/<id>/agents/<name>/import" \
-H "Authorization: Bearer hive_…" -H "X-Tenant-Id: <tenant id>" \
-H "Content-Type: application/json" \
-d '{ "node_id": "<org node id>" }'
# → 201 { agent, version, report: { completeness, unmapped_tools[], knowledge_refs[], suggested_slug, notes[], source } }# Migrate in (b): offline — paste a classic assistant JSON, a new agent object or an azure.yaml-shaped bundle. No Azure call.
curl -sS -X POST "https://<api-host>/v1/foundry/import-definition" \
-H "Authorization: Bearer hive_…" -H "X-Tenant-Id: <tenant id>" \
-H "Content-Type: application/json" \
-d '{
"definition": {
"object": "agent.version", "name": "tender-vendor-summary",
"definition": { "kind": "prompt", "model": "gpt-5-nano",
"instructions": "You screen tender bids for risk. Never invent a supplier name.",
"tools": [{ "type": "file_search" }] }
}
}'
# → 200 { dry_run: true, view, report, manifest } (default: nothing is persisted)
# add "persist": true + "node_id" → 201 { agent, version, report } (a draft, never published)The fidelity report is the important screen — it never claims a lossless copy:
{
"completeness": "partial",
"suggested_slug": "foundry-tender-vendor-summary",
"unmapped_tools": [
{ "type": "file_search",
"reason": "Vector store / index stays on Foundry until an explicit KB copy (M3)." },
{ "type": "code_interpreter",
"reason": "Enable code_execution on the draft after review; not auto-enabled." }
],
"knowledge_refs": [{ "kind": "vector_store", "id": "vs_…" }],
"notes": ["1 knowledge ref(s) stay on Foundry until an explicit copy."],
"source": { "surface": "agents", "id": "tender-vendor-summary", "name": "tender-vendor-summary", "kind": "prompt" }
}completeness is partial whenever a tool did not cross, a knowledge reference exists, or the Foundry kind has no portable prompt (hosted, container_app, workflow, external import as identity only). file_search / azure_ai_search map to the kb.search builtin with the corpus left on Foundry; function, openapi, mcp, azure_function, bing_grounding, a2a / connected_agent are listed with the reason and the suggested rebinding. The draft is an ordinary agent: versioned, publishable, with guardrails and audit.
Publish out — a platform agent on Foundry Agent Service
# Publish out: project a PUBLISHED platform agent onto the project as a Foundry prompt agent. Dry run first.
curl -sS -X POST "https://<api-host>/v1/foundry/connections/<id>/publish" \
-H "Authorization: Bearer hive_…" -H "X-Tenant-Id: <tenant id>" \
-H "Content-Type: application/json" \
-d '{ "agent_id": "<agent id>", "dry_run": true }'
# → { dry_run: true, hive_version: 3, plan: { method: "POST", path: "/agents", api_version: "v1",
# body: { name, description?, definition: { kind: "prompt", model, instructions, tools } },
# omitted: ["mcp:tender-tools (register as a Foundry Toolbox / MCP tool in M2)",
# "builtin:kb.search (Foundry file_search needs vector_store_ids; bind a vector store in M3)",
# "knowledge_bases[] (no silent corpus copy; bind a Foundry vector store in M3)"] } }
# Then for real (drop dry_run):
# → { result: { remote_id: "tender-drafting-assistant:1", name, version: "1", created: true, model, omitted[] }, plan_omitted[], hive_version }
# Re-publishing the same name: Foundry answers 409 on POST /agents; the platform falls back to POST /agents/{name}/versions,
# which returns the EXISTING version for an identical definition (version: "1", created: false) and mints ":2" only when it changed.
# 404 NOT_FOUND when the agent has no published version (drafts never cross).Only a published version may cross: published manifests are immutable, so the Foundry projection is reproducible; the platform stays the source of truth and the Foundry agent is a projection of it, never the reverse. omitted[] lists every capability that did not cross — MCP servers, A2A peers, knowledge bases and every builtin, kb.search included: Foundry rejects a bare file_search tool (400 Required property ‘vector_store_ids’ is missing), so nothing is sent for it until M3 binds a vector store. Observed live: Foundry answers POST /agents for an existing name with 409 conflict; the platform then calls POST /agents/{name}/versions, which returns the existing version for an identical definition (created: false, version: "1") and mints a new version only when the definition changed. created is derived from the returned version label. Foundry does not validate the model deployment at create time — a wrong name is accepted and fails only at invoke, so publish agents whose model exists on the project.
4. Reference — routes, shapes, roles
Base path /v1/foundry. Every route needs an API key and X-Tenant-Id (see API keys); rows are tenant-scoped by row-level security, so another tenant’s connection id is a 404. Roles: member may read, admin may write, test, invoke, import, link and publish.
| Route | Role | Body → response |
|---|---|---|
GET /v1/foundry/connections | member | → { items: FoundryConnection[] } |
POST /v1/foundry/connections | admin | { slug, name, project_endpoint, auth_kind, credentials, api_version?, region?, residency_region?, node_id?, default_surface? } → 201 { connection }; 409 SLUG_TAKEN |
GET · PATCH · DELETE /v1/foundry/connections/:id | member · admin · admin | PATCH: any create field plus enabled; credentials rotates the bag → { connection }; DELETE → 204 |
POST …/:id/test | admin | → { ok, surface, count }; persists last_inventory + default_surface |
GET …/:id/agents | member | → { surface, items: FoundryAgentView[] }; audited foundry.inventory |
GET …/:id/agents/:name | member | → { agent: FoundryAgentView } |
GET …/agents/:name/threads · GET …/agents/:name/runs?thread_id= | member | Classic surface only → { items }; 409 FOUNDRY_SURFACE_UNSUPPORTED on a new-agents-only project; thread_id is required for runs |
POST …/agents/:name/invoke | admin | { input (≤ 16 000 chars), thread_id? } → { result: FoundryInvokeResult } |
POST …/agents/:name/import | admin | { node_id, slug?, name? } → 201 { agent, version, report } (draft) |
POST …/agents/:name/link | admin | { slug?, trust_level = "partner" } → 201 { external_agent: { id, slug, name } }; 409 FOUNDRY_SURFACE_UNSUPPORTED on an api_key connection; 409 SLUG_TAKEN |
POST …/:id/publish | admin | { agent_id, dry_run = false } → dry run { dry_run: true, plan, hive_version }; real { result: FoundryPublishResult, plan_omitted[], hive_version }; 404 when the agent has no published version |
POST /v1/foundry/import-definition | admin | { definition, persist = false, node_id?, slug?, name? } → dry run { dry_run: true, view, report, manifest }; with persist: true (then node_id is required) 201 { agent, version, report } |
Shapes (from the API’s Zod schemas)
| Object | Fields |
|---|---|
FoundryConnection | id, slug, name, project_endpoint, api_version (default v1), region?, residency_region?, node_id? (null = tenant-wide), auth_kind, credential_meta (tenant_id / client_id / header / hint — never the secret), default_surface (agents | assistants | null = auto), last_synced_at, enabled, created_at, updated_at |
credentials by auth_kind | entra_client_credentials: { tenant_id, client_id, client_secret, scope? } (scope defaults to https://ai.azure.com/.default) — the production path; api_key: { api_key, header? } (sent in api-key unless header names another); bearer: { token } (a pre-minted Entra token). The last two are for local doubles; link refuses api_key. |
FoundryAgentView | surface, id, name, description?, kind (prompt | hosted | container_app | workflow | external | assistant), model?, instructions?, tools[] ({ type, name?, description? }), knowledge[] ({ kind: vector_store | azure_ai_search | file | toolbox, id, name? }), metadata?, version?, created_at?, a2a_endpoint? |
FoundryInvokeResult | text, status (completed | failed | cancelled | expired | requires_action), surface, remote_thread_id?, remote_run_id?, remote_response_id?, model?, usage? ({ input_tokens?, output_tokens? }, Responses surface only) |
FoundryImportReport | completeness (full | partial), unmapped_tools[] ({ type, name?, reason }), knowledge_refs[], suggested_slug, notes[], source ({ surface, id, name, kind }) |
FoundryPublishPlan | api_version: "v1", method: "POST", path: "/agents", body ({ name (≤ 63, alphanumeric ends, hyphens), description?, definition: { kind: "prompt", model, instructions, tools[] } }), omitted[] |
FoundryPublishResult | remote_id ({name}:{version}), name, version, created (true only when Foundry reports version 1), model?, omitted[] |
Error codes
| Code | Status | Meaning |
|---|---|---|
FOUNDRY_AUTH | 401 / 403 | Entra refused the client credentials, or the project refused the token (missing Foundry User role). |
FOUNDRY_UNREACHABLE | 502 · 504 · 409 | The project did not answer or answered 5xx (one bounded retry on idempotent GETs, never on POSTs); 409 when the connection is disabled. |
FOUNDRY_SURFACE_UNSUPPORTED | 409 | Classic threads / runs on a new-agents-only project; link on an api_key connection; a project that accepts neither publish path. |
FOUNDRY_AGENT_NOT_FOUND | 404 | No agent with that name on either surface. |
FOUNDRY_INVOKE_FAILED | 502 / 504 | Thread, message, run or Responses call failed; 504 when a classic run did not reach a terminal state in time. |
VALIDATION | 400 | Body validation, a project_endpoint that is not https on *.services.ai.azure.com, or runs without thread_id. |
NOT_FOUND | 404 | Unknown connection id (including another tenant’s), or a publish target with no published version. |
SLUG_TAKEN | 409 | Connection or external-agent slug already exists. |
5. Governance evidence
| What | Where |
|---|---|
| Every privileged action | audit_log: foundry.connection.create / update / delete · foundry.inventory · foundry.invoke · foundry.import · foundry.link · foundry.publish — ids, status, surface, latency, tool counts; never instructions, message bodies or credentials |
| Consumption | usage_events kind external_agent_call with source: foundry_project, connection id, remote ids, latency and the token counts Foundry reported |
| Tenant isolation | foundry_connections is row-level-secured (enable + force, USING + WITH CHECK); a second tenant asking for the same id gets 404 for reads and for publish |
| Secrets and egress | Decryption only inside the gateway per call; the project host is pinned to *.services.ai.azure.com with the strict SSRF policy and an empty insecure-host allowlist — stricter than the A2A path |
| Human approval | Imports default hitl.default to review; a linked agent is gated by grants, approval policies and its trust floor like any external agent. Nothing goes live silently. |
6. Not true yet — say it before they ask
- Verified live once (29 Sep 2026) against one GA project: inventory, inspect, invoke, import-to-draft and publish. Not yet observed live: a classic (Assistants-shaped) project, a project that needs the project-scoped Responses fallback (the agent-endpoint route worked with no
Foundry-Featuresheader), and hosted / container agents. - Publish is prompt agents only. Hosted / container publish needs an ACR and agent-identity contract that does not exist.
kb.searchis omitted on publish until M3 binds a vector store. - No corpus, Toolbox or eval copy.
file_searchvector stores stay on Foundry and are referenced; Toolbox → MCP server rows, eval suites and workflow graphs are M3 roadmap — copying classified corpora is an explicit decision, never a side effect. - No streaming of Foundry tokens;
/invokereturns the completed text. - No
traceparentproducer on platform runs yet — correlate by the remote ids inusage_events. - No console card; the public cloud host suffix only (sovereign-cloud suffixes are not allow-listed).
7. What a demo project costs on the Azure side
Microsoft list prices as of 29 Sep 2026. Agent Service charges no per-agent or per-run platform fee for prompt agents — you pay for tokens, metered tools and the optional standard-setup resources.
| Choice | Why |
|---|---|
| One AIServices S0 account + one project, basic agent setup | The standard setup provisions ≈ $250/month of Cosmos DB + AI Search you do not need for a demo. |
One tool-less prompt agent on gpt-5-nano Global Standard | $0.05 in / $0.40 out per 1M tokens — a light demo is ≈ $0.06/month; gpt-4.1-mini ≈ $0.32. gpt-4.1-nano retires 14 Oct 2026. gpt-5-nano cannot use Function / MCP / OpenAPI tools in Agent Service — use gpt-5.4-nano (≈ $0.21/month) when the agent needs tools. |
| No Application Insights, no metered tools | Bing grounding is $14 per 1 000 calls, Code Interpreter $0.03 per session, File Search $0.10 per GB-day — none is needed to show the four capabilities. |