Send Message Tool
Let agents manage the live conversation — send, react, open a thread, or edit / retract their own messages — through Telegram, Slack, Discord, and other gateways
Send Message lets a running agent reach the user proactively on their configured channels — without waiting for the next user turn.
Need the answer back, not just delivery? See Ask Conversation.
from praisonaiagents import Agent
from praisonaiagents.tools import send_message
agent = Agent(
name="Reporter",
instructions="When you finish a long task, message the user on their channel.",
tools=[send_message],
)
agent.start("Generate the weekly report and notify me when it's ready.")The user starts a long job; the agent proactively messages them on Telegram, Slack, or email when it finishes.
Quick Start
- Give Your Agent the Tool
python from praisonaiagents import Agent from praisonaiagents.tools import send_message agent = Agent( name="Reporter", instructions="When you finish a long task, message the user on their channel.", tools=[send_message], ) agent.start("Generate the weekly report and notify me when it's ready.")The agent can now call
send_messageat any point mid-task to push a notification to the user. - List Available Targets
python # The model calls: # send_message(action="list") # and gets back JSON like: # [{"target": "telegram:home", "platform": "telegram", "kind": "home", "label": "Telegram"}]Use
action="list"first when you want the agent to pick the right channel rather than defaulting to"origin". - Send with a File Attachment
python # Model call: # send_message("origin", "Weekly report ready MEDIA:/tmp/report.pdf") # Returns: "Sent to telegram:<chat_id> (media not attached)."Append
MEDIA:<path>to the message text to attach a local file. Multiple attachments are supported.
How It Works
| Component | Purpose |
|---|---|
| send_message tool | Agent-callable function; resolves messenger from context |
| OutboundMessengerProtocol | Interface the gateway registers per-turn |
| SessionContext | Task-local registry — no globals, safe for concurrent handlers |
| Gateway | Delivers to Telegram, Slack, Discord, WhatsApp, etc. |
The verbs react, thread, edit, and delete add a capability-gate step: the router checks the adapter’s supports_* flag before dispatch and returns a typed unsupported when the channel lacks the primitive — it never raises.
Targets
Choose the right target form for your situation:
| Target form | Example | Description |
|---|---|---|
"origin" | "origin" | The chat this conversation came from (default) |
"<platform>" | "telegram" | That platform’s configured home channel |
"<platform>:<chat_id>" | "slack:#ops" | A specific chat or channel |
"<platform>:<chat_id>:<thread_id>" | "slack:#ops:T123" | A specific thread within a chat |
"<alias>" | "my-ops-alert" | A named alias configured in the gateway |
Attachments (MEDIA:)
Append MEDIA:<path> to the message string to attach a local file to the delivery.
# Single attachment
send_message("origin", "Report ready MEDIA:/tmp/report.pdf")
# Multiple attachments — each MEDIA: token is stripped and the paths collected
send_message("origin", "Charts attached MEDIA:/tmp/chart1.png MEDIA:/tmp/chart2.png")Reactions
Agents can tap a lightweight ✅ / 👍 / ⏳ reaction on a message instead of sending a full reply — perfect for busy group chats where every reply is noise.
from praisonaiagents.tools import send_message
# React to the message being handled right now
send_message(action="react", target="origin", message="👍")
# Remove a reaction you added earlier
send_message(action="unreact", target="origin", message="👍")The emoji rides in the message argument and is .strip()-ed — an empty emoji returns {"status": "failed", "detail": "no emoji provided"}.
Reaction outcomes
Reactions return a JSON string of a ReactionResult with a typed status (plus optional target and detail):
status | Meaning |
|---|---|
"ok" | Reaction added/removed successfully |
"unsupported" | The channel doesn’t support reactions (e.g. WhatsApp) — degrade gracefully |
"failed" | Blocked by send-policy, empty emoji, or downstream error |
"no_route" | Target could not be resolved to a reachable channel |
{"status": "ok", "target": "telegram:12345"}
{"status": "unsupported", "detail": "the active gateway does not support reactions"}
{"status": "failed", "detail": "no emoji provided"}Channels that support reactions today: Telegram, Slack, Discord. Others (WhatsApp, Signal, email, agentmail, Linear) return "unsupported" — the tool never raises.
The same Send Policy that guards action="send" also guards react/unreact — a steered or prompt-injected agent cannot react on a channel the operator never intended.
Threads
Open a topic or thread under a target so a long-running effort has its own space.
from praisonaiagents.tools import send_message
# Open a topic titled "Rollout 2026-09" under the current Slack channel
send_message("slack:#ops", "Rollout 2026-09", action="thread")The thread title rides in the message argument. An empty title returns {"status": "failed", "detail": "no thread title provided"}.
Thread outcomes
Threads return a JSON ThreadResult with a typed status (plus thread_id on success):
status | Meaning |
|---|---|
"ok" | A new thread/topic was opened; thread_id carries its id |
"unsupported" | The channel can’t open threads — degrade gracefully to the parent channel |
"failed" | Topics mode off, missing permission, or empty title |
"no_route" | Target could not be resolved to a reachable channel |
{"status": "ok", "target": "slack:#ops", "thread_id": "T789"}
{"status": "unsupported", "detail": "the active gateway does not support threads"}
{"status": "failed", "detail": "no thread title provided"}Thread-aware routing across platforms is covered in Gateway Threads.
Edit and Delete (own messages)
Update a status in place, or retract a message the agent shouldn’t have sent — instead of spamming follow-ups.
from praisonaiagents.tools import send_message
# Post a live status, then update it in place instead of spamming follow-ups
send_message("origin", "Working…", action="send")
# ... later:
send_message("origin", "Done ✅", action="edit", message_id="98")
# Retract a message the agent shouldn't have sent
send_message("origin", "", action="delete", message_id="77")- Authority:
editanddeleteare the most-restricted class —own-prior-message. They only work on messages this agent/session sent. - Capability gate: powered by the adapter’s
supports_edit/supports_delete. Channels that can’t edit/delete return a typedunsupportedoutcome — the tool never raises. - Send-policy still guards
edit/delete— a steered agent cannot mutate messages on channels the operator hasn’t allowed. - Required
message_id: a missing id returns{"status": "failed", "detail": "no message_id provided"}. - Result shape: a JSON
MessageActionResultwithstatus(ok/unsupported/failed/no_route), optionaltarget, and optionaldetail.
{"status": "ok", "target": "telegram:12345"}
{"status": "unsupported", "detail": "the active gateway does not support edit"}
{"status": "unsupported", "detail": "the active gateway does not support delete"}
{"status": "failed", "detail": "no message_id provided"}Channel support today
| Channel | supports_edit | supports_delete |
|---|---|---|
| Telegram | ✅ | ✅ |
| Slack | ✅ | ✅ |
| Discord | ✅ | ✅ |
| WhatsApp / Signal / email / agentmail / Linear | unsupported | unsupported |
Per-channel capability flags live in Bot Platform Capabilities.
Action Vocabulary and Authority
Every verb the tool accepts declares one of three authority classes — a closed set that keeps a steered agent from inventing new mutation verbs.
| Authority | Verbs | What it means |
|---|---|---|
read-only | list | Inspects reachable targets, sends nothing |
current-conversation | send, react, unreact, thread | Writes to the current conversation (or an operator-permitted target) |
own-prior-message | edit, delete | Mutates a specific earlier message the agent sent — the most sensitive class |
Any verb missing from the map is rejected — the agent cannot “invent” a new mutation verb that silently inherits send‘s guard:
Unknown action '<verb>'. Use 'send', 'list', 'react', 'unreact', 'thread', 'edit', or 'delete'.The same Send Policy guards every write verb. Per-channel primitives are listed in Bot Platform Capabilities.
Listing Targets
action="list" returns a JSON array of all reachable targets so the agent can pick the right one before sending.
# Model call:
# send_message(action="list")
# Returns JSON:
# [
# {"target": "origin", "platform": "telegram", "kind": "origin", "label": "Current chat"},
# {"target": "telegram", "platform": "telegram", "kind": "home", "label": "Telegram"},
# {"target": "slack:#ops", "platform": "slack", "kind": "alias", "label": "#ops"}
# ]Each entry in the array:
| Field | Type | Description |
|---|---|---|
target | str | Token to pass as the target argument to send_message |
platform | str | Platform name (e.g. "telegram", "slack") |
kind | str | One of "origin", "home", "alias", or "observed" |
label | str | Friendly name for display |
"observed" means a channel the gateway has seen activity on but is not a configured home or alias.
When It’s Available
| Runtime | Behaviour |
|---|---|
| Bot / Gateway (Telegram, Slack, Discord, WhatsApp, …) | Delivers the message; returns a short summary string |
CLI / one-shot (agent.start(...) directly) | Returns the string below — does not raise |
When no gateway is active the tool returns:
No active gateway: send_message is only available inside a running bot/gateway (e.g. Telegram, Slack, Discord). It is unavailable for CLI/one-shot runs.Agent instructions can check for this string and fall back gracefully.
User Interaction Flow
A user starts a long research task on Telegram, then closes their phone. The agent finishes compiling the report, calls send_message("origin", "Your report is ready MEDIA:/tmp/report.pdf"), and the user receives a Telegram push notification with the text — without ever having to ask “are you done yet?”.
A user pings the agent in a 500-person Slack channel. Instead of replying and cluttering the thread, the agent taps a ⏳ reaction to acknowledge, works on the task, then swaps to ✅ when done — no message noise, but the user still sees progress.
An agent posts “Working…” on Telegram, keeps working, then edits that same message to “Done ✅” — the user sees a single evolving status line on their phone, not a stream of noisy follow-ups.
The agent notices it posted a message with the wrong figure and calls send_message("origin", "", action="delete", message_id="…") to unsend it before the user reads it.
Configuration Reference
send_message takes four arguments. All are optional:
| Argument | Type | Default | Description |
|---|---|---|---|
target | str | "origin" | Symbolic destination (see Targets table) |
message | str | "" | Text (for send), emoji (for react/unreact), thread title (for thread), or new text (for edit). Append " MEDIA:<path>" on send to attach files. |
action | str | "send" | One of "send", "list", "react", "unreact", "thread", "edit", "delete" |
message_id | str | "" | The id of the message to react to / edit / delete. Optional for react/unreact on origin (the inbound message is used). Required for edit/delete. |
action values at a glance:
action | message carries | message_id | Returns |
|---|---|---|---|
"send" | text (" MEDIA:<path>" optional) | — | Short summary string |
"list" | ignored | — | JSON array of {target, platform, kind, label} |
"react" | the emoji | optional (defaults to inbound msg on origin) | JSON ReactionResult |
"unreact" | the emoji to remove | optional | JSON ReactionResult |
"thread" | the thread title | — | JSON ThreadResult |
"edit" | the new text | required | JSON MessageActionResult |
"delete" | ignored | required | JSON MessageActionResult |
Common Patterns
Notify on long-task completion
agent = Agent(
name="Analyst",
instructions="""
Run deep analysis tasks. When a task finishes, always call
send_message("origin", "<summary>") to let the user know.
""",
tools=[send_message],
)Cross-channel handoff
# Model calls send_message(action="list") first, picks slack:#ops, then:
# send_message("slack:#ops", "Deploy finished ✅")Pick the target from the list, then send
# 1. List targets
# send_message(action="list")
# → [{"target": "slack:#ops", ...}, {"target": "telegram", ...}]
# 2. Choose and send
# send_message("slack:#ops", "All tests passed")Acknowledge with a reaction in a busy channel
agent = Agent(
name="Support",
instructions="""
In busy group chats, tap send_message(action="react", target="origin",
message="⏳") to acknowledge a request, then react with "✅" once the
task is done instead of posting a full reply.
""",
tools=[send_message],
)Post-then-edit a live status line
agent = Agent(
name="Deployer",
instructions="""
When a deploy starts, post 'Deploying…' with send_message("origin", "Deploying…").
Remember the returned message id, and when done, edit it to 'Deployed ✅' with
send_message("origin", "Deployed ✅", action="edit", message_id="<id>").
""",
tools=[send_message],
)Open a thread for a long-running rollout
send_message("slack:#ops", "Q4 rollout — sub-thread", action="thread")Best Practices
Confirm channel before sending to an explicit target
Call send_message(action="list") first when the user hasn’t specified where they want to be notified. Sending to a channel the user doesn’t monitor is noise.
If you build a target string from untrusted input (a tool result, a user message, a document), expect Refused: … — surface it to the user instead of retrying.
Trust the attested set
origin, the platform home, and aliases are the sanctioned destinations. An explicit <platform>:<chat_id> only works for chats the gateway knows (origin, home, alias, or observed) — see Destination Attestation.
Default to 'origin'
"origin" sends to the chat the conversation started in — usually the right choice. Only override it when you have a specific reason (e.g., sending an ops alert to a dedicated Slack channel).
Handle the no-gateway fallback in agent instructions
Include a note like _”If send_message returns a ‘No active gateway’ message, skip the notification and continue.”_ so the agent doesn’t retry endlessly on CLI runs.
Keep messages short — they hit a phone
Mobile push notifications truncate long text. Send a one-line summary; attach details as a file with MEDIA:.
Edit your own status instead of spamming replies
On channels that support it, edit keeps the chat clean; on channels that don’t, you’ll get unsupported and can post a follow-up.
Only edit / delete your own messages
Adapters reject mutating anyone else’s message with a transport failed.
Always check the returned status
Never assume ok. On unsupported or failed, fall back to a plain send.
Related
Request/reply sibling — message a target and wait for its answer
Send PolicyRestrict which channels send_message is allowed to deliver to
Destination AttestationWhy unknown <platform>:<chat_id> targets are refused
Ask the user mid-task for clarifying input
Channels GatewayConnect agents to Telegram, Slack, Discord, and WhatsApp
Bot GatewayRun the gateway server
Bot RoutingRoute messages by channel and platform
Gateway ThreadsThread-aware routing and creation across platforms
Bot Platform CapabilitiesPer-channel supports_edit / supports_delete / supports_reactions matrix
A stopped or superseded turn cannot send, react, thread, edit or delete