> ## Documentation Index
> Fetch the complete documentation index at: https://rasa.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# External Sub Agent

> Rasa supports stateful execution of external agents via A2A protocol.

<Warning>
  **New Beta Feature in 3.14**

  Rasa supports stateful execution of external agents via A2A protocol.

  This feature is in a beta (experimental) stage and may change in future Rasa versions.
  We welcome your feedback on this feature.
</Warning>

External sub agents connected via the [A2A (Agent-to-Agent) protocol](https://a2a-protocol.org/latest/) operate as autonomous entities that can handle complex, multi-turn conversations independently.
When invoked through a [`call` step](/docs/reference/primitives/flow-steps#autonomous-steps) in your flows, these agents take control of the conversation and interact with users until their task is complete.

## A2A Protocol Connection

Rasa connects to external sub agents through the A2A (Agent-to-Agent) protocol, which provides a standardized way for different AI agents to communicate and collaborate. Here's how the connection process works:

### Connection Process

1. **Agent Card Resolution**: Rasa first retrieves the external sub agent's capabilities by loading its agent card, which can be either:

   * A local JSON file path
   * A remote URL pointing to the agent card
2. **Client Initialization**: Rasa creates an A2A client configured with:

   * Authentication credentials (if required)
   * Supported transport protocols (JSON-RPC, HTTP JSON, gRPC)
   * Streaming capabilities for real-time communication
   * Timeout and retry settings
3. **Health Check**: Before establishing the connection, Rasa performs a health check by sending a test message to verify that the external sub agent is responsive and properly configured.
4. **Message Exchange**: Once connected, Rasa communicates with the external sub agent by:

   * Sending user messages and conversation context
   * Receiving responses and structured data
   * Handling task status updates and completion signals

### Request metadata on A2A messages

Rasa forwards the entire `AgentInput.metadata` dictionary on each outgoing A2A user [`Message`](https://a2a-protocol.org/latest/specification/) as `Message.metadata`. That lets your remote agent read orchestration or backend-only data—such as authentication tokens, tenant IDs, or feature flags—without putting those values in the message **parts** that drive the language model.

Rasa also sets `Message.context_id` and `Message.task_id` from the metadata entries with keys `context_id` and `task_id` (see `rasa.agents.constants`: `A2A_AGENT_CONTEXT_ID_KEY` and `A2A_AGENT_TASK_ID_KEY`). Those keys are used for session continuity across turns; you can supply additional keys the same way.

For **per-conversation secrets** (tokens, customer credentials), configure `pre_call_hook` in the agent `configuration` section (see [Call-time credential hooks (`pre_call_hook`)](/docs/reference/config/agents/external-sub-agents#call-time-credential-hooks-pre_call_hook) below). Rasa invokes the hook before each outbound `message/send` or `message/stream` and merges the result into `Message.metadata` without writing secrets to slots or the tracker.

To attach other non-secret orchestration metadata, override `process_input` on a custom `A2AAgent` class and merge into `input.metadata` before returning the `AgentInput`. The default metadata already includes values the flow executor maintains (for example sender id, agent id, and model id).

### Call-time credential hooks (`pre_call_hook`)

Add `pre_call_hook` under `configuration` in the external sub agent's `config.yml`. The value is a dotted import path to a module-level sync or async function. Use `async def` when the hook performs I/O (for example fetching from a secret store). You do not need a custom `A2AAgent` subclass unless you also customize other behaviour.

```yaml title="sub_agents/car_shopping_agent/config.yml" theme={null}
agent:
  name: car_shopping_agent
  protocol: A2A
  description: "Helps users shop for cars"

configuration:
  agent_card: ./sub_agents/car_shopping_agent/agent_card.json
  pre_call_hook: custom.call_time_credentials.resolve_a2a_shopping_meta
```

Rasa calls the hook as `hook(context)` before each outbound A2A message. Hook metadata is merged into [`Message.metadata`](https://a2a-protocol.org/latest/specification/). It is **not** placed in message `parts` or top-level `context_id` / `task_id`.

Import types from `rasa.shared.agents.outbound_call_hook`:

```python theme={null}
from rasa.shared.agents.outbound_call_hook import (
    A2AOutboundCallContext,
    OutboundCallResult,
)
```

The hook receives a frozen, read-only `A2AOutboundCallContext`:

| Field                   | Description                                                   |
| ----------------------- | ------------------------------------------------------------- |
| `sender_id`             | Conversation id — use as your secret-store lookup key         |
| `agent_id`              | Configured sub-agent id                                       |
| `context_id`, `task_id` | Read-only A2A session fields; do not overwrite in hook output |
| `turn_metadata`         | Snapshot of existing `AgentInput.metadata` (read-only input)  |

Return an `OutboundCallResult` with a `metadata` dict. `OutboundCallResult` has a single `metadata` field; Rasa merges it onto the wire. For convenience, a plain `dict` is accepted and normalized to `OutboundCallResult(metadata=...)`:

```python title="custom/call_time_credentials.py" theme={null}
async def resolve_a2a_shopping_meta(
    context: A2AOutboundCallContext,
) -> OutboundCallResult:
    customer_id = await lookup_customer_id(context.sender_id)
    return OutboundCallResult(metadata={"x-customer-id": customer_id})
```

<Tip>
  **AWS Secrets Manager or similar**

  Use `context.sender_id` as the stable lookup key your connector assigns to each conversation. Fetch and decrypt inside the hook only — do not copy secrets into domain slots.
</Tip>

If a hook raises an exception or returns an invalid type, Rasa aborts the outbound A2A call (the message is not sent). Rasa logs metadata **key names** at debug level, never values.

`rasa train` and bot validation resolve every configured `pre_call_hook` import path. If the path does not import a callable, validation fails with `validation.pre_call_hook.unresolved`.

### Supported Transport Protocols

The A2A protocol supports multiple transport mechanisms:

* **JSON-RPC**: Lightweight remote procedure calls over HTTP
* **HTTP JSON**: Simple HTTP-based JSON messaging
* **gRPC**: High-performance RPC framework with streaming support

During connection, the transport protocol is automatically selected, following the server’s preference when available, and otherwise falling back to the best available protocol.

### Authentication

External sub agents can require authentication for secure communication.
Rasa supports various authentication methods including API keys, OAuth 2.0, and pre-issued tokens, which are configured in the external sub agent's [configuration file](/docs/reference/config/agents/external-sub-agents#authentication).

### Long-running tasks with push notifications

Rasa can let long-running A2A tasks continue in the background when the external agent supports push notifications.
This keeps the Rasa conversation responsive while the remote task is running.
If the user sends another message before the external task finishes, Rasa triggers `pattern_external_agent_processing`; the default response is `utter_agent_busy`.

Rasa uses background push mode only when both of these conditions are true:

* The external agent card advertises push notification support with `capabilities.pushNotifications: true`.
* The external sub agent configuration includes `push_notification_url`.

If either condition is missing, Rasa falls back to the normal streaming or polling behavior.
The `streaming` capability can still be `true`; Rasa chooses background push mode only when push notifications are also available.

```json title="External agent card" theme={null}
{
  "capabilities": {
    "streaming": true,
    "pushNotifications": true
  }
}
```

Set `push_notification_url` to the public base URL of your Rasa server.
Rasa appends `/agent/task_update` when it registers the callback with the external A2A agent.
The `agent_card` value can be a local file path or a URL.

```yaml title="sub_agents/car_research/config.yml" theme={null}
agent:
  name: car_research
  protocol: A2A
  description: "Finds car options"

configuration:
  agent_card: ./sub_agents/car_research/agent_card.json
  push_notification_url: https://rasa.example.com
  max_polling_time: 60
  polling_initial_delay: 0.5
  # auth: ...  # Optional; uses the same auth shape as other external sub agents
```

Call the sub agent from a flow with the standard [`call` step](/docs/reference/primitives/flow-steps#autonomous-steps):

```yaml title="flows.yml" theme={null}
flows:
  research_cars:
    name: "car research"
    description: Help the user choose a car to buy by searching the web, and answer questions about specific cars as well as generic car-related questions.
    steps:
      - call: car_research
```

While the A2A task is on the stack, new user messages do not start another sub agent call.
Instead, Rasa runs `pattern_external_agent_processing`.
Override that pattern if you want a different message than `utter_agent_busy`.

#### Push notification delivery by channel

An A2A push notification is a task update from the external agent to Rasa.
It is separate from delivering a bot message to the user.
Rasa stores the task update on the tracker first, then the input channel decides whether it can deliver the pending message proactively.

Rasa always stores task updates in the tracker.
Whether the user receives a proactive notification depends on the channel that owns the conversation.

| Channel type                                                                                                           | Delivery behavior                                                                                                                                                                                                                                                                                                                      |
| ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Request-response channels, such as REST                                                                                | Rasa cannot proactively deliver a message because the channel only sends data to the user during a user-initiated request. Use a channel that supports server-initiated delivery, such as the [Callback channel](/docs/reference/channels/your-own-website#callbackinput), when users must receive updates without sending another message. |
| Server-initiated channels, such as [Callback](/docs/reference/channels/your-own-website#callbackinput), Slack, and Telegram | The Rasa worker that receives the A2A update can deliver the notification to the user.                                                                                                                                                                                                                                                 |
| Persistent-connection channels, such as voice channels, Socket.IO, and other WebSocket-based channels                  | Delivery requires the worker that receives the A2A update to also own the user's open connection. In Kubernetes deployments with multiple pods or multiple workers per pod, Rasa cannot guarantee that routing. If the receiving worker does not own the connection, the user does not receive a proactive notification.               |

When Rasa cannot deliver a proactive notification, the tracker still keeps the bot messages.
Rasa drains those messages to the user when the next user message arrives on the channel.

### Intermediate Messages

External sub agents connected via the A2A protocol can send intermediate messages when Rasa receives task updates with status `"submitted"` or `"working"`. These messages are

* **immediately sent to the user**: Intermediate messages are forwarded to the user as soon as they are received, without waiting for task completion.
* **tracked in conversation history**: Rasa automatically creates bot uttered events for these messages, ensuring they are properly tracked in the tracker store.

<Info>
  Ensure that intermediate messages sent by your A2A agent are meaningful and provide value to users. Messages should offer useful updates about task progress, status changes, or relevant information that helps users understand what the agent is doing.
</Info>

### Background Task Cancellation

Long-running A2A operations (both polling and streaming) run in the background and are cancelled automatically when the conversation reaches an inactive or terminal state.

Rasa cancels background A2A processing in the following cases:

* `ConversationInactive` is emitted by session timeout while an A2A task is still running.
* `SessionEnded` or `ConversationInactive` is appended through the tracker events API (`POST /conversations/{conversation_id}/tracker/events`, see [Rasa Pro REST API](/docs/reference/api/pro/http-api/tracker/append-events-to-a-tracker)).
* Closing the Inspector browser tab, which automatically cancels in-flight A2A operations for that conversation.

For details about session timeout and lifecycle events, see [Session Lifecycle](/docs/reference/config/session-management/session-lifecycle).

You can also cancel background A2A processing manually:

* From custom code (for example, a [custom connector](/docs/reference/channels/custom-connectors)), by calling `Agent.cancel_background_tasks(sender_id)`.
* From external systems, by calling `POST /cancel_background_tasks/<sender_id>` on the REST channel endpoint.

When cancellation is triggered (automatically or manually), the in-flight A2A operation is stopped promptly instead of continuing until polling timeout.

### Best Practices

An A2A agent can [respond with a `Task` or a `Message`](https://a2a-protocol.org/dev/topics/key-concepts/#agent-response-task-or-message).
`Task`s should be used for long-running operations and `Message`s for immediate responses.

Rasa maps all `Message` objects to `INPUT_REQUIRED` state, meaning the response is forwarded to the user while the external sub agent waits for input and continues running.

**Recommended Approaches:**

* **Task-only**: Use `Task`s for the complete workflow
* **Hybrid**: Use `Task`s for main operations and `Message`s **only** for clarifications

This approach aligns with [Google's recommendations](https://a2a-protocol.org/dev/topics/life-of-a-task/#agent-response-message-or-task) for agent design and provides better control over conversation flow and state management.

## Configuration

External sub agents extend the [basic sub agent configuration](/docs/reference/config/agents/overview-agents#configuration) with A2A protocol-specific settings.

In addition to the required `agent` section, external sub agents support the following configuration options:

```yaml theme={null}
# Basic agent information (required - see overview)
agent:
  name: car_shopping_agent
  protocol: A2A
  description: "Helps users shop for cars by connecting them with dealers and facilitating purchases"

# A2A-specific configuration
configuration:
  agent_card: ./sub_agents/car_shopping_agent/agent_card.json  # Required: path or URL to agent card
  module: "path.to.custom.module"  # Optional: custom module for sub agent customization
  push_notification_url: https://rasa.example.com  # Optional: public Rasa base URL for background push mode
  max_polling_time: 120  # Optional: max seconds to poll for task completion (default: 60)
  polling_initial_delay: 1.0  # Optional: starting delay between polls in seconds (default: 0.5)
  pre_call_hook: custom.call_time_credentials.resolve_a2a_shopping_meta  # Optional: call-time credential hook
  # auth: ...  # Optional: see Authentication below
```

### Configuration Parameters

The `configuration` section in an external sub agent's `config.yml` must include the required field **`agent_card`**. All other fields in this section are optional.

* **`agent_card` (required)**: Location of the agent card defined by the [A2A protocol](https://a2a-protocol.org/latest/specification/#57-sample-agent-card). It can be:

  * A local file path (relative to the project root), or
  * A URL pointing to the agent card JSON
* **`module` (optional)**: Path to a custom Python module for [sub agent customization](/docs/reference/config/agents/external-sub-agents#customization), using the same pattern as other sub agents. See [Creating a Custom Sub Agent](/docs/reference/config/agents/external-sub-agents#creating-a-custom-sub-agent).
* **`push_notification_url` (optional)**: Public base URL of the Rasa server used for background updates from an external A2A agent. Rasa appends `/agent/task_update` to this value. Background push mode requires this setting and an external agent card with `capabilities.pushNotifications: true`; otherwise Rasa falls back to normal streaming or polling. Use an HTTPS URL that the external agent can reach.
* **`max_polling_time` (optional, default: `60`)**: When Rasa polls an A2A task until it reaches a terminal state, this is the maximum total time in seconds to spend in that polling loop. Leave unset to use the default. When set, the value must be greater than zero.

<Info>
  If you increase `max_polling_time` for long-running A2A tasks, set `TICKET_LOCK_LIFETIME` to a value greater than or equal to `max_polling_time`.
  If the lock lifetime is shorter than the A2A polling window, the conversation lock can expire while the previous turn is still processing, and a new user message from the same sender can trigger concurrent tracker writes (race condition).
  See [Environment Variables](/docs/reference/config/environment-variables#backing-services) and [Lock Stores](/docs/reference/integrations/lock-stores).
</Info>

* **`polling_initial_delay` (optional, default: `0.5`)**: Starting delay in seconds between polling attempts while the task is not yet terminal; later waits increase with exponential backoff, capped at `5` seconds per polling interval. Leave unset to use the default. When set, the value must be greater than zero.
* **`pre_call_hook` (optional)**: Dotted import path to a sync or async function that returns call-time credential metadata for outbound A2A messages (for example `my_auth.credentials.resolve_a2a_meta`). Rasa merges the hook's `metadata` into `Message.metadata` before each send. Use this for per-conversation secrets instead of storing them in slots. See [Call-time credential hooks (`pre_call_hook`)](/docs/reference/config/agents/external-sub-agents#call-time-credential-hooks-pre_call_hook).
* **`auth` (optional)**: Credentials for connecting to a secured external agent. Supports an API key (Bearer or custom header), OAuth 2.0 client credentials, or a pre-issued token. See [Authentication](/docs/reference/config/agents/external-sub-agents#authentication) for examples and for which parameters must reference environment variables.

### Authentication

External sub agents support multiple authentication methods for connecting to external services:

* **API Key**: Static key attached as `Authorization: Bearer <token>`
* **OAuth 2.0 (Client Credentials)**: Automatic token retrieval with client ID/secret
* **Pre-issued Token**: Direct token usage until expiry

Configure authentication with **`configuration.auth`** in your `config.yml` file.
Below are several examples:

<Tabs>
  <Tab title="API Key">
    ```yaml theme={null}
    configuration:
      agent_card: ./sub_agents/shopping_agent/agent_card.json
      auth:
        api_key: "${API_KEY}"
    ```
  </Tab>

  <Tab title="API Key (Custom Header)">
    ```yaml theme={null}
    configuration:
      agent_card: ./sub_agents/shopping_agent/agent_card.json
      auth:
        api_key: "${API_KEY}"
        header_name: "X-API-Key"
        header_format: "{key}"
    ```
  </Tab>

  <Tab title="OAuth 2.0">
    ```yaml theme={null}
    configuration:
      agent_card: ./sub_agents/shopping_agent/agent_card.json
      auth:
        oauth:
          client_id: "${CLIENT_ID}"
          client_secret: "${CLIENT_SECRET}"
          token_url: "https://auth.company.com/oauth/token"
          scope: "read:users"
    ```
  </Tab>

  <Tab title="Pre-issued Token">
    ```yaml theme={null}
    configuration:
      agent_card: ./sub_agents/shopping_agent/agent_card.json
      auth:
        token: "${ACCESS_TOKEN}"
    ```
  </Tab>
</Tabs>

The `$` syntax is **required** for the following sensitive parameters:

* `api_key`
* `token`
* `client_secret`

This ensures that they are not stored in plain text in your configuration files.
For other parameters like `client_id`, using the `$` syntax is optional —
you can either reference an environment variable using the `$` syntax or provide the value directly in the configuration.

## Customization

External sub agents can be customized by subclassing `A2AAgent` and overriding public Python hooks. This lets you filter context sent to the remote agent, attach backend metadata, map structured A2A results into Rasa slots, or adjust how task artifacts become structured results.

Register the custom class in `configuration.module` (see [configuration](#configuration-parameters)).

### Customization hooks reference

| Hook                                                                  | When to override                                                                                 |
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| [`process_input`](#input-processing-customization)                    | Filter slots, enrich `AgentInput.metadata`, or reshape context before the A2A call               |
| [`process_agent_output`](#output-processing-customization)            | Map A2A structured results or response text into `SlotSet` events after the remote agent returns |
| [`get_structured_results_from_task`](#customizing-structured-results) | Control how task artifacts and status messages become `structured_results`                       |

<Note>
  `process_agent_output()` is invoked by Rasa only for A2A agents (not ReAct sub agents). ReAct agents use [`process_tool_output`](/docs/reference/config/agents/react-sub-agents#processing-tool-output) instead.

  To change which slots or metadata reach the remote agent, override `process_input()` — do not override private helpers used to build the outbound A2A `Message`.
</Note>

### Creating a Custom Sub Agent

To customize an external sub agent, create a Python class that inherits from `A2AAgent` and override the hooks you need:

```python title="A2AAgent subclass (skeleton)" theme={null}
from rasa.agents.protocol.a2a.a2a_agent import A2AAgent
from rasa.agents.schemas import AgentInput, AgentOutput

class CarShoppingAgent(A2AAgent):

    async def process_input(self, agent_input: AgentInput) -> AgentInput:
        ...

    async def process_agent_output(self, output: AgentOutput) -> AgentOutput:
        ...
```

Reference the class in the [external sub agent's configuration](/docs/reference/config/agents/overview-agents#configuration-file):

```yaml {8} title="config.yml (custom module)" theme={null}
agent:
  name: car_shopping_agent
  protocol: A2A
  description: "Helps users shop for cars by connecting them with dealers"

configuration:
  agent_card: ./sub_agents/car_shopping_agent/agent_card.json
  module: "sub_agents.car_shopping_agent.custom_agent.CarShoppingAgent"
```

<h3 id="input-processing-customization">
  Input Processing Customization
</h3>

Override `process_input()` to filter slots, inject metadata for the remote A2A server, or reshape context before Rasa sends the A2A message. Values in `AgentInput.metadata` are forwarded on the A2A `Message.metadata` field (not in LLM-visible message parts).

```python title="process_input — filter slots and attach metadata" theme={null}
from rasa.agents.protocol.a2a.a2a_agent import A2AAgent
from rasa.agents.schemas import AgentInput

class CarShoppingAgent(A2AAgent):

    async def process_input(self, agent_input: AgentInput) -> AgentInput:
        # Only forward slots the remote shopping agent needs
        keep = {"recommended_car_models", "recommended_car_details"}
        agent_input.slots = [s for s in agent_input.slots if s.name in keep]

        # Backend-only metadata (not sent as message text parts)
        agent_input.metadata["tenant_id"] = "premium-auto"
        agent_input.metadata["locale"] = "en-US"
        return agent_input
```

See the [common input-processing section](/docs/reference/config/agents/overview-agents#input-processing-customization) for the full `AgentInput` schema.

<h3 id="output-processing-customization">
  Output Processing Customization
</h3>

Override `process_agent_output()` to extract structured data from the A2A response and persist it as Rasa slot updates. This runs after each A2A turn completes and before the flow executor processes the result.

```python title="process_agent_output — map structured_results to slots" theme={null}
from typing import List

from rasa.agents.protocol.a2a.a2a_agent import A2AAgent
from rasa.agents.schemas import AgentOutput
from rasa.shared.core.events import SlotSet

class CarShoppingAgent(A2AAgent):

    async def process_agent_output(self, output: AgentOutput) -> AgentOutput:
        if not output.structured_results:
            return output

        slot_events: List[SlotSet] = []
        for iteration in output.structured_results:
            for entry in iteration:
                result = entry.get("result", {})
                decision = result.get("final_reservation_decision")
                if not decision or decision.get("final_decision") != "reserve":
                    continue
                slot_events.append(SlotSet("car_model", decision.get("car_model")))
                slot_events.append(SlotSet("car_price", decision.get("price")))
                slot_events.append(SlotSet("dealer_name", decision.get("dealer_name")))

        if slot_events:
            output.events = (output.events or []) + slot_events
        return output
```

<h3 id="customizing-structured-results">
  Customizing structured results
</h3>

Override `get_structured_results_from_task()` when the default artifact parsing does not match your remote agent’s payload shape. The default collects `DataPart` and `FileWithUri` content from task artifacts and status messages:

```python title="get_structured_results_from_task — custom artifact handling" theme={null}
from typing import Any, Dict, List, Optional

from a2a.types import Task
from rasa.agents.protocol.a2a.a2a_agent import A2AAgent
from rasa.agents.schemas import AgentInput

class CarShoppingAgent(A2AAgent):

    def get_structured_results_from_task(
        self, agent_input: AgentInput, task: Task
    ) -> Optional[List[List[Dict[str, Any]]]]:
        # Use default parsing first
        results = super().get_structured_results_from_task(agent_input, task)
        if results is None:
            return None

        # Example: normalize a vendor-specific artifact name
        for iteration in results:
            for entry in iteration:
                if entry.get("name", "").endswith("_callback_slots"):
                    entry["name"] = "callback_slots"
        return results
```

For more on bidirectional data flow, see [Context Sharing](/docs/reference/config/agents/overview-agents#context-sharing) in the sub agent overview.


## Related topics

- [Integrating External Agents via A2A](/docs/pro/build/integrating-external-agents.md)
- [Exposing Rasa as an A2A Sub-Agent](/docs/pro/build/exposing-rasa-as-a2a-sub-agent.md)
- [Overview](/docs/reference/config/agents/overview-agents.md)
- [Rasa as A2A Agent Architecture](/docs/reference/architecture/rasa-as-a2a-agent.md)
- [A2A Server](/docs/reference/integrations/a2a-server.md)
