# How the `send_message_to_user` Tool Enables Proactive Agent Messaging in AstrBot

> Learn how the AstrBot send_message_to_user tool empowers LLM agents to proactively message users with rich media and file attachments, enhancing communication beyond request-response.

- Repository: [AstrBot AI/AstrBot](https://github.com/AstrBotDevs/AstrBot)
- Tags: how-to-guide
- Published: 2026-03-12

---

**The `send_message_to_user` tool is a function-tool that allows AstrBot's LLM-driven agents to push messages to users outside the normal request-response flow, supporting rich media, file attachments, and sandbox file resolution.**

This tool is essential for building proactive AI agents in AstrBot that need to notify users when background tasks complete, cron jobs finish, or long-running operations yield results. According to the AstrBot source code, it serves as the sanctioned channel for all outbound proactive messaging, centralizing session management, file handling, and platform-specific delivery.

## Core Architecture of `send_message_to_user`

The tool is implemented as a first-class citizen in AstrBot's agent framework, with explicit registration, schema validation, and execution paths that bridge LLM decisions to concrete message delivery.

### Tool Definition and Schema

The **`SendMessageToUserTool`** class in [`astrbot/core/astr_main_agent_resources.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/core/astr_main_agent_resources.py) defines the contract that the LLM sees when deciding to contact a user. It declares the tool name, description, and a strict JSON schema for the `messages` parameter—an ordered list of message components that can include text, images, files, mentions, and other media types.

The class exposes a **`call()`** method (lines 70-84 and 92-124 in the same file) that performs the heavy lifting: validating the payload, resolving file paths (including automatic sandbox retrieval), constructing `MessageComponent` objects (`Plain`, `Image`, `Record`, `Video`, `File`, `At`), and ultimately dispatching the `MessageChain` to the target session.

### Execution Flow

When the LLM decides to send a message, **`FunctionToolExecutor._execute_local`** in [`astrbot/core/astr_agent_tool_exec.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/core/astr_agent_tool_exec.py) (lines 70-85) detects that the tool overrides `call()` and runs it synchronously within the agent's coroutine loop. This bridges the LLM-generated tool call JSON to the Python implementation without blocking the main event loop.

### Registration and Availability

The tool is automatically registered with every main-agent run. In [`astrbot/core/astr_main_agent.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/core/astr_main_agent.py), the system adds the tool to the provider request via `req.func_tool.add_tool(SEND_MESSAGE_TO_USER_TOOL)` (around lines 46-48). This ensures that both interactive sessions and background/cron agents can access proactive messaging capabilities.

The system prompt explicitly instructs the LLM to use this specific tool for immediate delivery:

```python
req.prompt = (
    "… If you need to deliver the result to the user immediately, "
    "you MUST use `send_message_to_user` tool to send the message directly to the user, "
    "otherwise the user will not see the result."
)

```

*Source: [`astrbot/core/astr_agent_tool_exec.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/core/astr_agent_tool_exec.py) lines 10-13*

## How the Tool Works Step-by-Step

The execution path from LLM decision to user notification follows a precise sequence:

1. **LLM Tool Call Generation** – The model returns JSON matching the schema, optionally specifying a target `session` or defaulting to the current session:

   ```json
   {
     "name": "send_message_to_user",
     "arguments": {
       "session": "12345",
       "messages": [
         {"type": "plain", "text": "Your job finished!"},
         {"type": "image", "path": "/tmp/result.png"}
       ]
     }
   }
   ```

2. **Executor Invocation** – `FunctionToolExecutor` routes the call to `SendMessageToUserTool.call(context, **arguments)`.

3. **Payload Validation** – The `call()` method verifies that `messages` is a non-empty list and that each entry contains a valid `type` field.

4. **Component Resolution** – For each message component:
   - **Plain** → Creates `Comp.Plain(text=...)`
   - **Media (Image/Video/Record/File)** → If a local `path` is provided, `_resolve_path_from_sandbox()` checks the host filesystem; if missing, it attempts to download from the sandbox environment. If a `url` is supplied, the component is created directly from the URL.
   - **Mention** → Creates `Comp.At(qq=...)` for user mentions.

5. **Chain Construction** – The resolved components are wrapped in a `MessageChain` object.

6. **Session Resolution** – The target session is determined via `MessageSession.from_str(session)` or the current `unified_msg_origin`.

7. **Platform Delivery** – The tool invokes `await context.context.context.send_message(target_session, MessageChain(chain=components))`, which routes through the appropriate platform adapter (e.g., Telegram, OneBot/QQ) to convert the chain into platform-specific API calls.

8. **Status Return** – The tool returns a confirmation string (`"Message sent to session ..."`) to the LLM.

## Practical Implementation Examples

### Direct Python Usage in Plugins

For custom plugin development, you can import the singleton tool instance directly:

```python
from astrbot.core.astr_main_agent_resources import SEND_MESSAGE_TO_USER_TOOL
from astrbot.core.astr_agent_context import AgentContextWrapper

async def proactive_notify(context: AgentContextWrapper):
    await SEND_MESSAGE_TO_USER_TOOL.call(
        context,
        session="user12345",
        messages=[
            {"type": "plain", "text": "⚡️ Your background job is done!"},
            {"type": "image", "url": "https://example.com/report.png"},
            {"type": "mention_user", "mention_user_id": "user12345"},
        ],
    )

```

*Key implementation details:* Tool definition and `call()` logic reside in [`astrbot/core/astr_main_agent_resources.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/core/astr_main_agent_resources.py) (lines 70-84 and 92-124).

### LLM-Driven Tool Invocation

When the LLM autonomously decides to notify a user, it emits JSON similar to:

```json
{
  "name": "send_message_to_user",
  "arguments": {
    "messages": [
      {"type": "plain", "text": "Your data export is ready, see the file below."},
      {"type": "file", "path": "/sandbox/exports/report.csv"}
    ]
  }
}

```

The executor automatically resolves `/sandbox/exports/report.csv` through the sandbox retrieval mechanism, builds a `File` component with the downloaded temporary path, and delivers it to the current user session without requiring the LLM to know the underlying filesystem details.

### Proactive Cron Job Messaging

In scheduled background tasks, the tool is injected into the agent's toolset to enable proactive notifications:

```python

# Inside cron job initialization (astrbot/core/cron/manager.py, lines 278-342)

req.func_tool.add_tool(SEND_MESSAGE_TO_USER_TOOL)

# Later, after job completion

await SEND_MESSAGE_TO_USER_TOOL.call(
    run_context,
    messages=[{"type": "plain", "text": "Daily summary generated successfully!"}]
)

```

This pattern ensures that cron jobs and background agents can push results to users even when no active conversation context exists.

## Summary

- **`send_message_to_user`** is the sanctioned channel for proactive outbound messaging in AstrBot, enabling LLM agents to push notifications outside the request-response cycle.
- The tool supports rich multi-component messages including **Plain** text, **Image**, **Video**, **Record**, **File** (with sandbox resolution), and **At** mentions.
- Core implementation resides in [`astrbot/core/astr_main_agent_resources.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/core/astr_main_agent_resources.py), with execution handled by `FunctionToolExecutor` in [`astrbot/core/astr_agent_tool_exec.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/core/astr_agent_tool_exec.py).
- Automatic registration in [`astrbot/core/astr_main_agent.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/core/astr_main_agent.py) ensures availability for all main-agent runs, including cron jobs managed by [`astrbot/core/cron/manager.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/core/cron/manager.py).
- The tool centralizes file path resolution, sandbox handling, and platform-specific delivery through the `MessageChain` abstraction.

## Frequently Asked Questions

### Can `send_message_to_user` target any user, or only the current session?

The tool accepts an optional `session` parameter. If provided, it resolves the target via `MessageSession.from_str(session)`. If omitted, it defaults to the current `unified_msg_origin` context. This allows agents to message specific users when they have the session identifier, or default to the active conversation.

### How does the tool handle files created in the sandbox environment?

The **`_resolve_path_from_sandbox()`** method in [`astrbot/core/astr_main_agent_resources.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/core/astr_main_agent_resources.py) checks if the provided path exists on the host filesystem. If the file is missing locally but exists in the sandbox, the method automatically downloads it from the sandbox environment to a temporary location before constructing the message component.

### Is this tool available to all AstrBot agents by default?

Yes. According to [`astrbot/core/astr_main_agent.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/core/astr_main_agent.py) (lines 46-48), the tool is automatically added to every main-agent provider request via `req.func_tool.add_tool(SEND_MESSAGE_TO_USER_TOOL)`. This includes both interactive chat agents and background/cron agents initialized through [`astrbot/core/cron/manager.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/core/cron/manager.py).

### What happens if the LLM tries to send an invalid message format?

The `call()` method validates that the `messages` argument is a non-empty list and that each entry contains a valid `type` field. Invalid payloads are rejected before any platform API calls are made, preventing malformed messages from reaching the user.