Build your first agent in just a few minutes with [Rasa Copilot](https://hello.rasa.com/?utm_source=docs&utm_medium=referral&utm_campaign=docs_cta).

On this page

New in Rasa Pro 3.17

Rasa can expose your assistant as a native [Agent-to-Agent (A2A)](https://a2a-protocol.org/latest/) 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](/content/docs/reference/config/agents/external-sub-agents/index.html) and [Integrating External Agents via A2A](/content/docs/pro/build/integrating-external-agents/index.html).

## Basic Configuration

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

```yaml
a2a_server:
  url: "http://localhost:5005"          # optional; inferred from --port / --interface / --ssl-certificate
  description: "My banking sub-agent"
  include_conversation_repair: true     # false → terminal completed instead of repair input_required
  task_timeout_seconds: 600             # per-task safety net; 0 disables
  a2a_message_cache_ttl_seconds: 600    # messageId replay TTL; defaults to task timeout
  max_contexts: 1000                    # in-memory session cap; 0 disables
```

## Configuration Reference

| Key | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `description` | string | — | **yes** | Public description for the AgentCard. Used for auto-generated and placeholder cards. Ignored when `agent_card_path` points to a static AgentCard JSON file. |
| `url` | string | inferred from `--port`, `--interface`, and `--ssl-certificate` | no | Public base URL of the Rasa server as seen by A2A orchestrators, advertised in the AgentCard (scheme, host, port, path prefix). Set explicitly when the public hostname or port differs from the local bind address (for example behind a reverse proxy or ingress). |
| `agent_card_path` | string | none (auto-generate from flows) | no | Path to a static AgentCard JSON file. When set, the file is loaded at startup and its `url` is overwritten with the resolved public URL. |
| `include_conversation_repair` | boolean | `true` | no | When `true`, pattern flows (for example `pattern_completed`) are advertised as skills and map to `input_required` so orchestrators know Rasa handles conversation repair. When `false`, the same stack state maps to `completed` instead. |
| `task_timeout_seconds` | integer | `600` | no | Maximum seconds each in-flight `task_id` may run before the server auto-cancels that task (safety net; use explicit `tasks/cancel` as the primary path). Timers are per task and are not extended by later `message/send` on the same `contextId`. Set to `0` to disable. |
| `a2a_message_cache_ttl_seconds` | integer | same as `task_timeout_seconds` | no | TTL in seconds for `messageId` deduplication cache entries keyed by `(contextId, messageId)`. Set to `0` for immediate expiry (effectively disables replay). |
| `max_contexts` | integer | `1000` | no | Memory guard: maximum distinct `contextId` sessions retained until a terminal task outcome. `input_required` keeps the session reserved. Set to `0` to disable the cap. |
| `push_notifications_enabled` | boolean | `false` | no | When `true`, the AgentCard advertises push notifications and the server may POST task updates to orchestrator-supplied callback URLs. Disabled by default because callback URLs are client-controlled and can be an SSRF vector. |
| `push_notification_allowed_hosts` | list of strings | none | no | Optional hostname allowlist for push callback URLs. When set, only `http`/`https` URLs whose host matches an entry (exact or subdomain) are accepted. Loopback and private-network targets are always rejected. |
| `auth` | object | none (open endpoint) | no | Bearer JWT authentication for orchestrators calling the A2A endpoint. |

## 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.

```yaml
domain.yml
session_config:
  session_expiration_time: 60
  start_session_after_expiry: false  # required when a2a_server is enabled
```

`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.

### 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`](/content/docs/reference/integrations/a2a-server/#multi-replica-load-balancing/index.html) instead of increasing Sanic workers per pod.

```bash
SANIC_WORKERS=1 rasa run -m models/your-model.tar.gz --endpoints endpoints.yml
```

### 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:**

| Requirement | Why |
| --- | --- |
| `SANIC_WORKERS=1` on every replica | A2A state is per-worker as well as per-pod. |
| Sticky routing keyed on `contextId` | Keeps in-memory A2A state coherent for each orchestrator context. |
| Orchestrator reuses `contextId` across turns | Already required for multi-step flows; stickiness depends on a stable key. |

### 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`.
