Responses vs. Invocations: Choosing the Right Protocol for Microsoft Foundry Hosted Agents
Every hosted-agent tutorial starts the same way: spin up a chat loop, wire it to a model, deploy, done. What those tutorials rarely tell you is that the moment your agent needs to receive a GitHub webhook, power an AG-UI frontend, or run as a background job triggered by Durable Functions, the "just use the chat SDK" advice falls apart. You end up hand-rolling HTTP handlers on top of a framework that was never designed for arbitrary payloads — or you fight the framework to make it behave like a webhook receiver.
Microsoft Foundry's hosted agent runtime avoids this trap by making the wire protocol a first-class architectural decision instead of an implementation detail. Before you write a line of agent logic, you choose between two protocols — Responses and Invocations — and that choice determines how sessions are tracked, how streaming works, who owns retries, and ultimately how much undifferentiated plumbing you have to build yourself.
This article is a deep, code-level look at both protocols: what problem each one solves, how they're implemented under the hood, when to pick one over the other, and what breaks in production if you pick wrong.
Why This Matters
Hosted agents are Foundry's answer to "I already have agent code — a LangGraph graph, a CrewAI crew, a hand-rolled orchestrator — and I want to run it as a managed, observable, RBAC-scoped service without becoming an infrastructure team." You containerize your agent, register it with a manifest, and Foundry takes care of provisioning, identity, scaling, and telemetry.
But "managed agent hosting" is not one shape of problem. A customer-support chatbot, a Stripe webhook processor, and a Durable Functions orchestration step all need an agent runtime — but they need completely different HTTP contracts. A protocol that's perfect for conversational chat (managed history, conversation_id threading, lifecycle events) is actively hostile to a webhook handler that needs to accept Stripe's exact JSON shape and return within a strict timeout. Conversely, a protocol built for raw HTTP control forces you to reimplement session storage and streaming plumbing for a simple Q&A bot that didn't need any of that complexity.
Foundry's designers made this trade-off explicit instead of hiding it, which means the "why" behind your integration architecture is knowable up front rather than discovered through trial and error three sprints into a project.
Table of Contents
- Core Concepts: Two Protocols, One Runtime
- Architecture: How a Hosted Agent Is Actually Deployed
- The Responses Protocol In Depth
- The Invocations Protocol In Depth
- Framework Choice: Agent Framework, LangGraph, or Bring Your Own
- Real-World Developer Scenario: One Agent, Two Callers
- Production Considerations
- Security Considerations
- Performance and Scalability
- Cost Considerations
- Common Mistakes and Pitfalls
- Alternatives and Trade-offs
- Practical Recommendations
- Conclusion
- References
Core Concepts: Two Protocols, One Runtime
A Foundry hosted agent is a container that speaks one (or both) of two HTTP contracts:
| Responses | Invocations | |
|---|---|---|
| Contract | OpenAI-compatible /responses
|
Arbitrary JSON via /invocations
|
| Session/history | Platform-managed via conversation_id
|
You manage it (in-memory, Redis, Cosmos DB) |
| Streaming | Framework-managed ResponseEventStream (created, in_progress, delta, completed) |
Raw Server-Sent Events — you format and write every chunk |
| Long-running work | Built-in background: true with platform-managed polling and cancellation |
Manual task tracking and custom polling endpoints |
| Server SDK |
azure-ai-agentserver-responses (or framework-native hosting packages) |
azure-ai-agentserver-invocations |
| Client | Any OpenAI-compatible SDK, zero custom code | Custom client that matches whatever schema you define |
The critical design insight is that both protocols run on the same underlying hosted-agent infrastructure — the same container runtime, the same RBAC model, the same observability pipeline, the same azd-driven deployment flow. You're not choosing a different product; you're choosing a different HTTP contract in front of identical infrastructure. A single agent manifest can even expose both protocols simultaneously, which matters more than it sounds like it should — more on that in the real-world scenario.
Architecture: How a Hosted Agent Is Actually Deployed
Before comparing protocols, it's worth understanding the deployment unit, because the protocol choice is declared right there in the manifest, not buried in code.
Every hosted agent ships with an azure.yaml manifest that azd reads to provision the Foundry project, the model deployment, and the container registry entry:
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
requiredVersions:
azd: '>=1.27.1'
extensions:
azure.ai.agents: '>=1.0.0-beta.9'
name: agent-framework-agent-basic-responses
services:
ai-project:
host: azure.ai.project
deployments:
- name: gpt-5.4-mini
model:
format: OpenAI
name: gpt-5.4-mini
version: '2026-03-17'
sku:
name: GlobalStandard
capacity: 10
agent-framework-agent-basic-responses:
host: azure.ai.agent
project: src/agent-framework-agent-basic-responses
language: python
codeConfiguration:
runtime: python_3_13
entryPoint: main.py
uses:
- ai-project
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
env:
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
container:
resources:
cpu: '0.5'
memory: 1Gi
infra:
provider: microsoft.foundry
The protocols block is the whole story: it's a list, not a single value, and each entry has its own version. That's the mechanism that lets one deployed agent expose /responses and /invocations side by side — you simply list both protocol blocks and implement both handlers in your container.
azd ai agent init -m <path-to-manifest> scaffolds a local project from this file, azd up (or azd provision + azd deploy) builds the container, pushes it to ACR, registers the agent version in Foundry, and wires up the managed identity and RBAC roles the agent needs to call its model deployment. Locally, azd ai agent run starts the same container on localhost:8088, and azd ai agent invoke --local "hi" talks to it — so the protocol you test against locally is exactly the protocol running in the cloud, container image and all.
[IMAGE: Architecture diagram showing a central "Microsoft Foundry Hosted Agent" container box, with two branching protocol paths — left labeled "Responses Protocol" flowing to an OpenAI-compatible SDK client with icons for managed conversation history, streaming lifecycle events, and background polling; right labeled "Invocations Protocol" flowing to a webhook/custom caller (GitHub, Stripe, Durable Functions) with icons for custom JSON payload, raw SSE, and manual session store. Beneath the container, show framework options: Agent Framework, LangGraph, Bring Your Own.]
The Responses Protocol In Depth
Responses is the default, and for good reason: it maps directly onto the OpenAI Responses API contract, so any OpenAI-compatible SDK becomes a working client with zero glue code. The platform — not your container — owns conversation history, event lifecycle, and background-task polling.
Here's a complete, minimal Responses-protocol agent using Agent Framework's Foundry hosting integration:
# main.py — Responses protocol, Agent Framework
import os
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from agent_framework_foundry_hosting import ResponsesHostServer
from azure.identity import DefaultAzureCredential
from dotenv import load_dotenv
load_dotenv()
def main():
model_name = os.getenv("AZURE_AI_MODEL_DEPLOYMENT_NAME") or os.getenv("FOUNDRY_MODEL_NAME")
if not model_name:
raise RuntimeError(
"Model deployment name is not configured. Set "
"AZURE_AI_MODEL_DEPLOYMENT_NAME or FOUNDRY_MODEL_NAME."
)
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=model_name,
credential=DefaultAzureCredential(),
)
agent = Agent(
client=client,
instructions="You are a friendly assistant. Keep your answers brief.",
# `store: False` disables agent-side history storage because the
# hosting infrastructure already manages conversation state via
# conversation_id. Duplicating storage here would be wasted work
# and a source of state drift.
default_options={"store": False},
)
server = ResponsesHostServer(agent)
server.run() # Listens on 0.0.0.0:8088 inside the container
if __name__ == "__main__":
main()
There are three things worth calling out, because they're the parts that surprise developers coming from a raw-HTTP mindset:
-
store: Falseis not optional boilerplate. TheResponsesHostServeralready threadsconversation_idthrough the platform's session store. If your agent also tries to persist history internally, you get two divergent copies of the conversation state — one that the platform replays on retries and one your code thinks is authoritative. Pick one owner of history; for Responses, that owner is the platform. -
Streaming lifecycle is a state machine you don't write. The client receives
created→in_progress→delta(repeated) →completedevents without your code emitting a single one of them. Compare that to Invocations, where you hand-format every SSE frame yourself. -
background: truegives you async execution for free. A client can setbackground: trueon the initial request and pollGET /responses/{id}until it's done, with cancellation handled by the platform. This is the same mechanism the Day 1 article's crash-resilient long-running agents build on for durability — Responses gives you the polling contract; your agent's checkpointing logic gives you the resumability.
Responses also supports two adjacent protocols that piggyback on the same session/streaming plumbing: Activity (for Teams/M365 channel integration) and A2A (agent-to-agent delegation, where a Responses-protocol caller invokes another hosted agent exposed as an A2A endpoint through a Foundry Toolbox a2a_preview tool). If you're building multi-agent systems, this is worth knowing — A2A delegation between two hosted agents is a Responses-protocol feature, not a separate hosting mode.
The Invocations Protocol In Depth
Invocations exists for exactly the cases where Responses is the wrong shape: webhook receivers, batch/classification jobs with structured JSON in and out, custom streaming protocols like AG-UI, and orchestration callers (Durable Functions, Logic Apps) that send task payloads, not chat turns.
Here's the equivalent minimal agent, but as an Invocations handler:
# main.py — Invocations protocol, Agent Framework
import os
from collections.abc import AsyncGenerator
from agent_framework import Agent, AgentSession
from agent_framework.foundry import FoundryChatClient
from azure.ai.agentserver.invocations import InvocationAgentServerHost
from azure.identity import DefaultAzureCredential
from dotenv import load_dotenv
from starlette.requests import Request
from starlette.responses import JSONResponse, Response, StreamingResponse
load_dotenv()
# In-memory session store keyed by session ID.
# WARNING: lost on restart/scale-out. Use Cosmos DB or Redis in production —
# see "Production Considerations" below.
_sessions: dict[str, AgentSession] = {}
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=DefaultAzureCredential(),
)
agent = Agent(
client=client,
instructions="You are a friendly assistant. Keep your answers brief.",
default_options={"store": False},
)
app = InvocationAgentServerHost()
@app.invoke_handler
async def handle_invoke(request: Request):
"""Handle streaming multi-turn chat via a custom /invocations contract."""
data = await request.json()
session_id = request.state.session_id # Platform-assigned per-caller session id
stream = data.get("stream", False)
user_message = data.get("message", None)
if user_message is None:
error = "Missing 'message' in request"
if stream:
return StreamingResponse(content=error, status_code=400)
return Response(content=error, status_code=400)
# We own session lookup/creation — the platform only gives us the ID.
session = _sessions.setdefault(session_id, AgentSession(session_id=session_id))
if stream:
async def stream_response() -> AsyncGenerator[str]:
async for update in agent.run(user_message, session=session, stream=True):
yield update.text # We format every SSE chunk ourselves
return StreamingResponse(
stream_response(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
)
response = await agent.run([user_message], session=session, stream=stream)
return JSONResponse({"response": response.text})
if __name__ == "__main__":
app.run()
Notice how much more this handler is responsible for compared to the Responses version: it defines its own request schema (message, stream), owns session lookup, and manually assembles the SSE stream. That's the entire point of Invocations — full HTTP control in exchange for doing the plumbing yourself. If a caller sends a Stripe webhook payload instead of {"message": "..."}, you just change what handle_invoke parses; there's no OpenAI-shaped contract to fight.
InvocationAgentServerHost still gives you the pieces that are genuinely platform concerns regardless of payload shape: TLS termination, the per-request session_id assignment, container lifecycle, and integration with Foundry's managed identity so DefaultAzureCredential() resolves without embedding secrets. What it deliberately does not give you is a schema, a history model, or a streaming event lifecycle — because those are the parts that vary by caller.
Framework Choice: Agent Framework, LangGraph, or Bring Your Own
Protocol and framework are orthogonal decisions — both protocols work with all three supported hosting paths:
-
Agent Framework (
agent-framework-foundry-hosting) — the most tightly integrated path. Native session, tool, memory, and streaming wiring, and it's the framework the platform team builds new capabilities against first (Foundry Toolbox, declarative YAML workflows, content-safety guardrails, downstream Azure service calls via per-agent Entra identity). It also natively hosts AutoGen and Semantic Kernel orchestrators. Start here unless you have existing code elsewhere. -
LangGraph (
langchain_azure_ai.agents.hosting) — if you already have aStateGraphor acreate_agentLangGraph agent,ResponsesHostServer/InvocationsHostServerfromlangchain-azure-ai[hosting]exposes it over either protocol with native Foundry session and streaming wiring, no rewrite required. -
Bring Your Own (
azure-ai-agentserver-core/-responses/-invocations) — for CrewAI or a fully custom stack, in any language. The core adapter hosts the web server and exposes/invocationsand/responses; you supply the agent logic behind it. This is the path when your agent isn't Python, or isn't built on any of Foundry's first-party integrations at all.
The practical decision tree: new build → Agent Framework. Existing LangGraph code → the LangGraph hosting package. Existing CrewAI/custom/non-Python code → Bring Your Own. In all three cases, the Responses-vs-Invocations decision from the sections above is unchanged — you're picking a protocol adapter on top of whichever framework you land on.
Real-World Developer Scenario: One Agent, Two Callers
Consider a support-ticket triage agent for an internal platform team. It needs to do two things:
- Answer engineers' ad-hoc questions in a chat UI ("why is ticket #4521 flagged as P1?").
- Automatically triage new tickets the instant a Zendesk webhook fires, writing a structured
{priority, owner_team, summary}JSON payload back to the ticketing system.
These are the same underlying agent — same instructions, same knowledge base, same model — but two fundamentally different callers. Rather than building two separate services, you list both protocols in one azure.yaml:
services:
triage-agent:
host: azure.ai.agent
project: src/triage-agent
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
- protocol: invocations
version: 2.0.0
env:
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
Inside the container, the chat UI talks to /responses and gets managed history, streaming, and conversation_id threading for free — no session code needed for that path. The Zendesk webhook hits /invocations, where your handler validates Zendesk's exact payload shape, extracts the ticket body, calls the same underlying Agent instance, and returns the structured JSON Zendesk's automation rules expect — no chat semantics, no OpenAI-shaped contract to shoehorn a webhook into.
This is the pattern the azure.yaml schema was clearly designed to support, and it's a strong argument for not defaulting to "just Responses, it's simpler" — the moment you have a non-chat caller on the roadmap, a single dual-protocol agent is less operational surface area than two separately deployed services that have to stay in sync on instructions, tools, and model version.
Production Considerations
-
Session storage is your responsibility on Invocations, always. The sample above uses an in-memory
dict, and the code comment isn't decorative — it will silently lose every active conversation on a pod restart or a scale-out event that routes a follow-up request to a different replica. Production Invocations agents need externalized session state (Cosmos DB, Redis) keyed by the platform-assignedsession_id, exactly the same durability problem covered for background agent work in general. - Idempotency for webhook-triggered Invocations agents. Webhook senders (GitHub, Stripe, Zendesk) retry on timeout or non-2xx. If your handler has side effects (writing tickets, sending notifications), dedupe on the sender's event ID before acting, not after.
- Observability comes free either way. Every hosted-agent sample ships with Application Insights and OpenTelemetry tracing out of the box — you get distributed traces and metrics from the first deployment, regardless of protocol. Don't skip wiring this into your own custom spans for handler-specific logic (payload validation, downstream calls) — the platform's default traces cover the agent runtime, not your webhook parsing code.
- Content-safety guardrails attach at the agent level, not the protocol level. If you need a Responsible AI content-safety policy screening prompts and responses, it's configured once and applies regardless of which protocol callers use.
Security Considerations
-
No embedded secrets. Both samples authenticate via
DefaultAzureCredential()against a per-agent managed identity provisioned byazd. There is no API key or connection string in the container image or environment — RBAC roles are assigned automatically when you deploy. Don't undo this by hardcoding a key "just for local testing" and forgetting to remove it. -
Invocations agents are your HTTP surface — treat them like one. Because you own the handler, you also own input validation. A webhook receiver that trusts
request.json()blindly is a webhook receiver with an injection vector. Validate schema and size before passing anything into the agent's context window or into a downstream tool call. - Downstream Azure service calls should use the agent's own identity, not a shared service principal. The Agent Framework samples show calling Blob Storage and Service Bus using the per-agent Entra identity with no connection strings — replicate that pattern for any Invocations handler that reaches into other Azure resources, so audit logs attribute actions to the specific agent, not a shared credential.
- A2A delegation expands your trust boundary. If a Responses-protocol agent delegates to another hosted agent over A2A, that second agent runs with its own identity and its own guardrails — verify both agents enforce content-safety and RBAC independently rather than assuming the caller's guardrail policy propagates.
Performance and Scalability
Responses' managed background execution (background: true) means the platform handles polling and cancellation without you holding a connection open — this scales better than a naive synchronous request for anything that might run past typical HTTP timeout windows (tool calls chaining into multi-second retrieval, multi-step reasoning). Invocations gives you the same capability only if you build it: your own task table, your own polling endpoint, your own cancellation semantics. That's real engineering effort you're taking on in exchange for payload flexibility.
Streaming has an inverse trade-off: Responses' framework-managed ResponseEventStream is simpler to consume but less flexible to shape — you get created/in_progress/delta/completed, not a custom event vocabulary. Invocations' raw SSE lets you emit whatever event types a custom frontend protocol (AG-UI, a proprietary streaming UI) actually needs, at the cost of writing and maintaining that formatting code yourself.
At the infrastructure layer, both protocols run on identical container resources (the samples default to 0.5 vCPU / 1Gi memory), so raw compute scaling behaves the same — this isn't a protocol-level performance difference, it's a request-shape one. In-memory session dictionaries on Invocations agents become a scaling anti-pattern the moment you run more than one replica, since session affinity isn't guaranteed across restarts or scale events; externalize session state before you scale out, not after you get paged for it.
Cost Considerations
Cost is driven primarily by model token consumption and container compute, and that's largely protocol-agnostic — a gpt-5.4-mini deployment at GlobalStandard capacity 10 costs the same whether requests arrive via /responses or /invocations. The indirect cost differences worth planning for:
- Responses' managed background polling avoids paying for idle held-open connections on long-running work, versus a naive synchronous Invocations handler that blocks a container thread (and its allocated compute) for the duration of a slow tool chain. (verify current billing granularity for hosted-agent compute before finalizing cost projections — container billing models evolve.)
- Externalized session storage for Invocations agents adds a cost line item (Cosmos DB RU/s or Redis instance) that Responses agents don't need, since the platform absorbs that storage cost as part of the managed session service.
- Dual-protocol agents (one container, two protocol blocks) are cheaper than two separately deployed agents doing the same underlying work, since you're not duplicating container compute, model deployment capacity reservations, or observability pipeline overhead across two services.
Common Mistakes and Pitfalls
- Defaulting to Invocations "for flexibility" on a plain chatbot. If your only caller is a chat UI, Invocations means reimplementing session storage, streaming formatting, and background polling that Responses gives you for free. Flexibility you don't need is just unpaid engineering work.
-
Defaulting to Responses for a webhook receiver and fighting the OpenAI contract. Trying to shoehorn Stripe's or GitHub's exact payload shape into a
/responsesrequest means transforming payloads twice (webhook → chat message → your actual logic) for no benefit — Invocations exists precisely so you don't do this. -
Forgetting
store: Falseon a Responses agent and ending up with two divergent copies of conversation history — one platform-managed, one agent-managed — that disagree after a retry or a multi-replica race. -
Leaving Invocations session state in a process-local
dictpast the prototype stage, then being surprised when a scale-out event or restart wipes every active conversation. - Not validating Invocations payloads because "the caller is trusted" — webhook senders get compromised, retried, and malformed just like any other HTTP client.
-
Assuming protocol choice is permanent. It isn't — you can add a second protocol block to an existing
azure.yamland implement the corresponding handler later. Don't over-design for a caller type you don't have yet; add Invocations when you actually get a webhook requirement, not speculatively.
Alternatives and Trade-offs
Outside Foundry's hosted-agent runtime entirely, you could run your agent as a plain Azure Container App or Function App fronted by your own API Management layer, handling auth, session state, and observability by hand. That gives you unconstrained control over the HTTP contract — genuinely more flexible than either Responses or Invocations — at the cost of losing Foundry's managed identity provisioning, built-in Application Insights/OpenTelemetry wiring, azd-driven deployment, and integration with Foundry Toolbox tools and content-safety guardrails. For teams already standardized on Foundry for other agents, that's rarely a good trade; for teams with heavy existing investment in a different orchestration platform (their own Kubernetes operators, an existing API gateway with agent-specific policies), it may still make sense to host outside Foundry and call in via Invocations-shaped Bring Your Own patterns, or skip hosted agents entirely.
Within Foundry itself, the alternative to hosted agents is the traditional Agent Service (threads, runs, and platform-managed tool execution without a container you control) — appropriate when you don't need custom code, arbitrary dependencies, or non-OpenAI-compatible protocols at all. Hosted agents exist specifically for the case where you need your own container and your own dependency stack; if you don't need that, Agent Service's zero-container model is simpler.
Practical Recommendations
- Start every new hosted agent with Responses, unless you already know you have a non-chat caller (webhook, custom streaming UI, structured batch job).
- Add Invocations as a second protocol block the moment a real non-chat caller shows up — don't build it speculatively, and don't force that caller into the Responses contract.
- Pick Agent Framework for new builds; reach for the LangGraph or Bring Your Own hosting packages only when you're carrying forward existing agent code that isn't Agent Framework-native.
- Externalize Invocations session state to Cosmos DB or Redis before your first production scale-out event, not after.
- Keep
store: Falseon Responses agents unless you have a specific, deliberate reason to duplicate history locally. - Treat every Invocations handler as a public HTTP endpoint from a security-review standpoint, because it is one.
Conclusion
The Responses-vs-Invocations decision in Microsoft Foundry hosted agents is really a decision about who owns complexity: the platform, or your code. Responses trades contract flexibility for a managed session, streaming, and background-execution model that gets an OpenAI-compatible chat agent to production with almost no plumbing. Invocations trades that managed convenience for full HTTP control, which is exactly what you need the instant a caller isn't speaking chat — a webhook, a batch job, a custom streaming protocol, or an orchestration engine sending structured task payloads.
The fact that both run on the same container, the same RBAC model, and the same manifest — and that a single agent can expose both simultaneously — means this isn't a one-way door. Pick the protocol that matches today's caller, and add the other one the day a second caller shape actually shows up.
If you're evaluating hosted agents for the first time, clone one of the basic samples, run it locally with azd ai agent run, and deliberately try to break the assumptions above — send a malformed payload, kill the container mid-conversation, scale it to two replicas — before you decide which protocol your production agent needs.
References
- Microsoft Foundry hosted agent samples —
microsoft-foundry/foundry-samples,samples/python/hosted-agents/(GitHub) - Hosted agents concept documentation — Microsoft Learn
- Quickstart: Create a hosted agent — Microsoft Learn
- Deploy a hosted agent — Microsoft Learn
- Manage hosted agents — Microsoft Learn
- Agent Framework — GitHub
-
langchain_azure_ai.agents.hosting— GitHub - OpenAI Responses API reference (protocol compatibility baseline) — developers.openai.com
Top comments (0)