Skip to main content
New in Rasa Pro 3.17Rasa can expose your assistant as a native Agent-to-Agent (A2A) sub-agent.
When an a2a_server block is present in endpoints.yml, rasa run registers A2A JSON-RPC routes on the same Sanic port as REST and channel webhooks. External orchestrators discover your assistant’s capabilities via AgentCard, send turns over JSON-RPC, and receive A2A task lifecycle updates mapped from Rasa’s dialogue state. This page documents the sub-agent (server) side. For Rasa as an orchestrator that calls external A2A agents, see External Sub Agent and Integrating External Agents via A2A.

Basic Configuration

Add a2a_server to your endpoints.yml file. Only description is required; other fields have sensible defaults.

Configuration Reference

Prerequisites

Rasa enforces two startup guardrails when a2a_server is configured. Both fail fast with actionable error messages.

Session configuration

When A2A is enabled, domain.session_config.start_session_after_expiry must be false. Resumed orchestrator contextId values reuse the same Rasa sender_id. If start_session_after_expiry is true, Rasa runs action_session_start after inactivity and can silently reset slot state when the orchestrator resumes the same context. This is validated when a model is loaded with a2a_server in endpoints.yml (for example during rasa run via load_agent). The error code is validation.a2a_server.incompatible_session_config.
domain.yml
ConversationInactive does not release an input_required A2A context. With start_session_after_expiry: false, the next message on the same contextId continues the flow after inactivity. See Session Timer for full session configuration details.

Sanic workers

SANIC_WORKERS=1 is required until a persistent solution for storing A2A tasks and messages ships. Multiple Sanic workers break messageId idempotency, in-flight HTTP 409 handling, orchestrator cancel, and max_contexts enforcement because these are per-worker only. This is validated when starting the Sanic server (rasa run). The error code is validation.a2a_server.incompatible_sanic_workers. Scale horizontally with additional replicas and load balancer sticky routing by contextId instead of increasing Sanic workers per pod.

Multi-replica load balancing

When a2a_server is enabled and you run more than one Rasa replica, configure your ingress or load balancer so all A2A JSON-RPC traffic for a given contextId routes to the same pod. A2A V1 keeps task state, messageId deduplication caches, in-flight turn queues, push notification callback registration, and max_contexts enforcement in memory on each replica. The shared tracker store (for example PostgreSQL) persists dialogue history, but A2A-specific state is not yet replicated across pods. Without sticky routing, follow-up turns on the same contextId may land on a different pod — the session can appear fresh, idempotency breaks, tasks/cancel may miss in-flight work, and push callbacks registered on one pod are invisible to others. Requirements for multi-replica A2A: When routing is configured correctly, every request for a given contextId hits the same pod. In-memory stores, queues, deduplication, push config, and the first-turn context owner then stay on that pod.

Deriving the routing key

Your load balancer must resolve a sticky key from contextId on every A2A POST / request. Use at least one of these sources (in order of typical precedence): Hash-route on the resolved contextId. Round-robin, source IP, and a load balancer’s own session cookie do not keep one A2A context on one replica. The a2a-context-id cookie below is only a way to carry that contextId when a later request omits it.

Istio on Kubernetes

Apply two Istio resources when Rasa runs behind an Istio ingress with multiple replicas. Istio consistentHash accepts one hash key per DestinationRule (header, cookie, source IP, or query parameter) — use a gateway EnvoyFilter to normalize contextId into a single internal header. 1. DestinationRule — consistent-hash on that header for the Rasa Service:
2. EnvoyFilter — on the ingress gateway (namespace: istio-system, selector istio: ingressgateway). Insert a Lua HTTP filter before envoy.filters.http.router that:
  • Applies only to POST requests on your assistant hostname (for example assistant.example.com).
  • Resolves contextId in order: "contextId" in the JSON body → X-A2A-Context-Id request header → a2a-context-id cookie.
  • Sets request header x-a2a-context-id to the resolved value (the DestinationRule hash key).
  • Stores the value in Envoy dynamic metadata during the request phase.
  • On response, emits Set-Cookie: a2a-context-id=<contextId>; Path=/; Max-Age=3600; HttpOnly; SameSite=Lax so methods like tasks/cancel that omit body contextId still route to the pod that handled prior turns.
Apply both resources with kubectl apply. Adjust spec.host and the hostname guard in the Lua filter to match your release name, namespace, and ingress host. Verify: With at least two replicas and SANIC_WORKERS=1, send two message/send turns with the same contextId — the second should continue the dialogue, not restart it. Confirm in pod logs that each contextId appears on one pod only. Exercise tasks/cancel with X-A2A-Context-Id or cookie persistence (curl -c / -b).

Other ingress and load balancers

On NGINX Ingress, AWS ALB, HAProxy, or other gateways, implement equivalent behavior:
  • Inspect POST / JSON bodies for "contextId" where present.
  • Accept X-A2A-Context-Id as a fallback for methods that only pass task_id.
  • Optionally set and honor an a2a-context-id cookie for clients that do not send contextId on every request.
  • Hash-route on that contextId to a single backend pod. Do not substitute the gateway’s session cookie or source-IP affinity.

Orchestrator and HTTP client guidance

  • Reuse the same contextId across turns in a multi-step flow.
  • For tasks/cancel and similar calls that only pass task_id in the body, send X-A2A-Context-Id: <contextId> or rely on cookie persistence through your ingress.
  • HTTP test clients should preserve cookies (curl -c / -b) when exercising sticky ingress.

HTTP Surface

A2A routes are registered at the root of the Rasa server (no URL prefix).

Context and task mapping

  • contextId in A2A requests maps to Rasa sender_id. One persistent Rasa conversation exists per orchestrator context.
  • Each orchestrator message gets a new task_id.
  • input_required keeps the context reserved for follow-up turns on the same contextId.

AgentCard Generation

Orchestrators discover your assistant’s capabilities through the public AgentCard.

Auto-generated (default)

When agent_card_path is omitted, Rasa auto-generates the AgentCard from user-facing flows at server startup (after the model loads):
  • Each user flow becomes an AgentSkill with id, name, and description (from the flow description or readable name).
  • Pattern/system flows (pattern_*) are included only when include_conversation_repair: true (the default).
  • name is taken from assistant_id in config.yml, or defaults to "Rasa Agent".
  • version is derived from the loaded model filename (without .tar.gz suffix), or from model_id, or defaults to "1.0.0".
  • capabilities.streaming is always true.
  • capabilities.push_notifications reflects push_notifications_enabled.

Static override

Set agent_card_path to load a static AgentCard JSON file. Rasa still overwrites the url field at startup with the resolved public URL from url or from --port / --interface / --ssl-certificate. The description field in endpoints.yml is ignored when a static card is used.

Refresh timing

At rasa run startup:
  1. Before the model loads, a placeholder AgentCard is served (empty skills array).
  2. After the model loads, the flow-generated or static AgentCard replaces it.
Hot reload via PUT /model does not refresh the AgentCard or re-wire the A2A executor. Restart the server after deploying a new model to update advertised skills and version.

Task States and Structured Output

Rasa maps dialogue stack and tracker state to A2A task states: working, input_required, completed, failed, canceled, rejected, and auth_required. Terminal and interactive-terminal updates include:
  • A DataPart with structured state: state, active_flow, current slots, and (when applicable) persisted slot values from flows that declare persisted_slots.
  • A TextPart carrying the user-visible bot utterance when present, so clients that only read status.message still receive NLG.
message/stream emits working status and artifact deltas during token/chunk streaming custom actions. Blocking message/send returns the final task.

Orchestrator operations

  • tasks/cancel — idle cancel on an input_required context, or signal in-flight work; publishes canceled and records FlowCancelled when a user flow was active.
  • messageId deduplication — retries with the same (contextId, messageId) replay the cached terminal task without re-running the turn. A concurrent duplicate while in flight returns HTTP 409 with a JSON-RPC error (retry after the first completes).

Orchestrator Slot Pre-seeding

On every message/send / message/stream turn, Rasa reads slot context from the incoming A2A Message, applies it as SetSlotCommands before dialogue processing, and strips slot markers from the user text so they are not passed to the LLM. Orchestrators can supply slots in any of these shapes. Later sources override earlier ones on key conflicts within the payload:
  1. Text transcript (lowest precedence) — append to the text part:
    • SLOTS: {"slot_name": "value", ...} on its own line, or
    • a trailing fenced block: ```json\n{"slot_name": "value"}\n```
  2. message.metadata — {"slots": {"slot_name": "value", ...}} (merged with JSON-RPC request metadata).
  3. DataPart (highest precedence) — {"kind": "data", "data": {"slots": {"slot_name": "value", ...}}}.
Only slots defined in the assistant domain are written; unknown, builtin, and agent-internal slots are ignored. Values are coerced to the slot type; invalid or out-of-range values (for example categorical mismatches) are skipped. String values "none", "null", and "undefined" are stored as null. Per-turn precedence: if command generation extracts a SetSlotCommand for a slot from the current user message, that value wins over orchestrator metadata for the same slot on that turn. Orchestrator values still pre-fill slots the parser did not set. Send an orchestrator slot snapshot on each follow-up message for slots the user did not answer in that turn. Do not rely on user-visible text alone to carry slot values when a collect step is active — prefer metadata.slots or a DataPart, or keep slot JSON out of text the user is answering with.

Authentication

When a2a_server.auth is set, every protected request must include Authorization: Bearer <jwt>. Missing or invalid tokens receive HTTP 401 with a WWW-Authenticate: Bearer response header before message processing starts. This applies to the JSON-RPC endpoint (POST /) and AgentCard discovery (GET /.well-known/agent-card.json, including the deprecated alias). If auth is omitted, behaviour is unchanged and no token is required. Only type: bearer is supported (API key auth is not available). Auth validates the orchestrator only; end-user identity belongs in A2A message metadata or slot pre-population, not in this JWT.

JWT configuration

Orchestrators and local test clients must send the same bearer token when fetching the AgentCard and when calling message/send, message/stream, or other A2A JSON-RPC methods.

Context ownership

When auth is set, the authenticated caller that first uses a contextId owns that conversation. The caller is the JWT sub, otherwise preferred_username, otherwise client_id. A validated token with none of those claims is rejected with 401, rather than treated as an anonymous caller: without this, every such token would resolve to the same shared identity and those callers could reach each other’s conversations and tasks. This is a backward-incompatible change for any orchestrator that was relying on a sub-, preferred_username-, and client_id-less JWT — add one of those claims to its token. A later message/send or message/stream from a different caller is rejected as context not found. The first message may still choose its own contextId, and a message that omits contextId still starts a session. Without auth, context ids are not bound to a caller. The first caller is reserved in the server process before its turn starts, and the owner is recorded when that turn is stored. Another replica can still accept a racing first message for the same contextId until that owner is recorded. Route every request for a contextId to the same replica, as described in Multi-replica load balancing. The routing key is that A2A contextId. A generic load-balancer session cookie, source IP, or connection affinity does not provide it, because those keys can group different contexts together or split one context across replicas. A conversation that has no owner recorded yet, such as one started before auth was set, is claimed by the next authenticated caller. Follow-ups need a token for the same caller. If Rasa cannot read an existing conversation, it rejects the request rather than treating it as unclaimed.
Rotating the JWT key invalidates existing A2A contextsOwnership is bound to the JWT verification material: auth.jwt.secret for HS256 and HS512, or the public key at auth.jwt.public_key_path for RS*, ES*, and PS*. Replacing that secret or public key changes the owner recorded for every conversation. The same caller no longer matches contexts they already own, and a follow-up message/send or message/stream is rejected as context not found. Finish or abandon those conversations before you rotate the key, or keep the previous verification material until they end.The owner also includes which JWT claim supplied the caller identity (sub, preferred_username, or client_id). Changing the claim used for an existing caller likewise invalidates its contexts.

Push Notifications

Push notifications are disabled by default (push_notifications_enabled: false) because callback URLs are client-controlled and can be an SSRF vector when the A2A endpoint is reachable by untrusted callers.

Enabling push notifications

Set push_notifications_enabled: true in endpoints.yml to advertise the capability in the AgentCard and allow orchestrators to register callback URLs:
When push notifications are enabled:
  • An orchestrator supplies a pushNotificationConfig.url on message/send, message/stream, or via tasks/pushNotificationConfig/set.
  • Rasa POSTs task state updates to that callback as the task progresses.
  • message/stream emits intermediate states (submitted, working) as well as the terminal state.
  • The public AgentCard advertises the push_notifications capability.
  • Rasa also POSTs the terminal canceled task when a callback URL is registered, because the a2a-sdk supplied on_cancel_task does not send push notifications.
Redirects are disabledRasa disables HTTP redirect following on every outbound push notification POST. URL policy is enforced only on the registered callback URL — redirect targets are not re-validated.What happens today: if the callback responds with a 3xx redirect (for example 302 with a Location header), httpx does not follow it. Rasa treats the delivery as failed (non-2xx response), logs the error, and does not retry. The A2A task itself still completes normally — a failed push does not change the task outcome returned to the orchestrator over JSON-RPC or SSE. Only a single POST is sent to the registered URL; no second request is made to the redirect target.Why redirects are disabled: if Rasa followed redirects, an orchestrator could register a callback URL that passes policy checks (public hostname, global IP), then respond with Location: http://127.0.0.1/... or another internal address. Rasa would POST task state — including structured slot data — to that internal target without running URL policy on the redirect destination. That is a classic server-side request forgery (SSRF) vector when the A2A endpoint is reachable by untrusted callers.Register the final callback URL directly. If the callback endpoint moves, update the registered pushNotificationConfig.url on the orchestrator side.

URL validation

Callback URLs are validated on registration and before each POST:
  • Only http/https URLs with a hostname are accepted.
  • A callback that sets a token or authentication must use https. An http callback is accepted only when it sets neither. Enabling push notifications does not stop rasa run.
  • Secret headers are not sent to an http callback.
  • tasks/pushNotificationConfig/get and tasks/pushNotificationConfig/list return the config with token and authentication.credentials cleared. Creating or replacing a config still returns what you sent. To update a config, send the token and credentials again.
  • Loopback, link-local, and private-network targets are rejected (including hostnames that resolve to those addresses via DNS).
  • URLs with embedded credentials are rejected.
  • HTTP redirects are not followed on outbound push POSTs (see note above).
  • Outbound push POSTs use an explicit httpx timeout (5s connect, 30s overall).
  • DNS checks are bounded by a 5s application timeout.
Optionally restrict callbacks further with push_notification_allowed_hosts (exact hostname or subdomain match). Rejected URLs surface as InvalidParamsError on the JSON-RPC methods that register them.
DNS rebindingDNS validation at registration and before each POST does not pin the resolved IP for the outbound connection; httpx performs a separate lookup at connect time (DNS rebinding TOCTOU). Keep the feature disabled unless required, use push_notification_allowed_hosts, prefer HTTPS callbacks, and treat callback registration as a trusted orchestrator action.

TLS

A2A shares the same port as REST, channels, and other Sanic routes. Configure HTTPS with the usual rasa run flags — not under a2a_server in endpoints.yml. A legacy tls block in endpoints.yml is rejected at startup.

Rasa serves HTTPS directly

Clients connect to Rasa over TLS using your certificate and key (the same cert covers REST, channels, and A2A):
When url is omitted, the AgentCard URL scheme follows --ssl-certificate. Set an explicit url: "https://..." when the public hostname or port differs from the local bind address.

HTTPS handled by a reverse proxy or ingress

Set url to the public base URL orchestrators use. Rasa can listen on plain HTTP behind the proxy while the AgentCard advertises HTTPS:

Monitoring and Troubleshooting

Health checks

Key log events

Rasa emits structured log events prefixed with a2a_server.:

Common failure modes

Complete Example

Out of Scope

Not tested for V1Running Rasa as an A2A sub-agent while also invoking external sub-agents (sub_agents/ with protocol: a2a) is not tested for V1. Use Rasa in one role at a time: either orchestrator or sub-agent.
Multi-worker A2A support (persistent task and message stores) is planned for a future release. Until then, use SANIC_WORKERS=1 per replica.