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.

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 user starts a long job; the agent proactively messages them on Telegram, Slack, or email when it finishes.

Diagram

Send Message Flow

🤖 Agent

📤 send_message

🛰️ Gateway

📱 User

Quick Start

  1. 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_message at any point mid-task to push a notification to the user.

  2. 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".

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

Diagram
UserGatewayOutboundMessengerSessionContextsend_messageAgentUserGatewayOutboundMessengerSessionContextsend_messageAgentalt[unknown destination][origin / home / alias / observed]alt[no gateway][gateway active]send_message(“origin”, “Done MEDIA:/tmp/r.pdf”)get_outbound_messenger()messenger (or None)“No active gateway: …”send(target, text, media=[…])is_attested?DeliveryResult(ok=False, “Refused: …”)“Refused: …”deliverpush (Telegram/Slack/etc.)DeliveryResult(ok, target, summary)summary
ComponentPurpose
send_message toolAgent-callable function; resolves messenger from context
OutboundMessengerProtocolInterface the gateway registers per-turn
SessionContextTask-local registry — no globals, safe for concurrent handlers
GatewayDelivers 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.

Diagram
AdapterDeliveryRoutersend_messageAgentAdapterDeliveryRoutersend_messageAgentalt[adapter supports primitive][primitive unavailable]send_message(…, action=”edit”/”delete”/”react”/”thread”)dispatch verbedit / delete / react / create_threadMessageActionResult(status=”ok”){“status”: “ok”}{“status”: “unsupported”}

Targets

Choose the right target form for your situation:

Diagram

No, same chat as this conversation

Platform’s default channel

Specific chat ID

Specific thread in a chat

Named alias

Which target to use?

Do you know the exact chat?

origin

<platform>

<platform>:<chat_id>

<platform>:<chat_id>:<thread_id>

<alias>

Target formExampleDescription
"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.

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

python
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):

statusMeaning
"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
json
{"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.

Diagram

deny

allow

no

yes

🤖 Agent

📤 send_message action=react

🛡️ Send Policy

❌ failed

🔍 Channel supports reactions?

⚠️ unsupported

🛰️ Gateway

👍 Emoji added


Threads

Open a topic or thread under a target so a long-running effort has its own space.

python
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):

statusMeaning
"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
json
{"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"}
Diagram

no

yes

🤖 Agent

📤 send_message action=thread

🔍 Channel supports threads?

⚠️ unsupported

🛰️ Gateway

🧵 Thread opened

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.

python
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: edit and delete are 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 typed unsupported outcome — 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 MessageActionResult with status (ok / unsupported / failed / no_route), optional target, and optional detail.
json
{"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

Channelsupports_editsupports_delete
Telegram✅✅
Slack✅✅
Discord✅✅
WhatsApp / Signal / email / agentmail / Linearunsupportedunsupported
Diagram

deny

allow

no

yes

🤖 Agent

📤 send_message action=edit/delete

🛡️ Send Policy

❌ failed

🔍 supports_edit / supports_delete?

⚠️ unsupported

🛰️ Adapter primitive

✅ ok

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.

AuthorityVerbsWhat it means
read-onlylistInspects reachable targets, sends nothing
current-conversationsend, react, unreact, threadWrites to the current conversation (or an operator-permitted target)
own-prior-messageedit, deleteMutates 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'.
Diagram

read-only

list

current-conversation

send

react

unreact

thread

own-prior-message

edit

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.

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

FieldTypeDescription
targetstrToken to pass as the target argument to send_message
platformstrPlatform name (e.g. "telegram", "slack")
kindstrOne of "origin", "home", "alias", or "observed"
labelstrFriendly 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

Diagram

Yes — Telegram, Slack, Discord, etc.

No — CLI or one-shot run

Runtime

Gateway running?

✅ send_message works

⚠️ Returns no-gateway message

RuntimeBehaviour
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?”.

Diagram
Gateway🤖 Agent📱 User (Telegram)Gateway🤖 Agent📱 User (Telegram)Closes Telegram, goes offlineGets the text notification (file delivery coming soon)“Generate the weekly sales report”Works on report…send_message(“origin”, “Done! Report attached MEDIA:/tmp/report.pdf”)📬 Telegram message “Done! Report attached”

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.

Diagram
Gateway🤖 Agent📱 User (Slack)Gateway🤖 Agent📱 User (Slack)“@agent summarise last night’s incident”send_message(action=”react”, target=”origin”, message=”⏳”)⏳ appears on the user’s messageWorks on the summary…send_message(action=”unreact”, target=”origin”, message=”⏳”)send_message(action=”react”, target=”origin”, message=”✅”)✅ swapped in — done, no extra reply

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.

Diagram
Gateway🤖 Agent📱 User (Telegram)Gateway🤖 Agent📱 User (Telegram)send_message(“origin”, “Working…”)📬 “Working…” (message id 98)Works on the task…send_message(“origin”, “Done ✅”, action=”edit”, message_id=”98″)✏️ same message now reads “Done ✅”

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:

ArgumentTypeDefaultDescription
targetstr"origin"Symbolic destination (see Targets table)
messagestr""Text (for send), emoji (for react/unreact), thread title (for thread), or new text (for edit). Append " MEDIA:<path>" on send to attach files.
actionstr"send"One of "send", "list", "react", "unreact", "thread", "edit", "delete"
message_idstr""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:

actionmessage carriesmessage_idReturns
"send"text (" MEDIA:<path>" optional)—Short summary string
"list"ignored—JSON array of {target, platform, kind, label}
"react"the emojioptional (defaults to inbound msg on origin)JSON ReactionResult
"unreact"the emoji to removeoptionalJSON ReactionResult
"thread"the thread title—JSON ThreadResult
"edit"the new textrequiredJSON MessageActionResult
"delete"ignoredrequiredJSON MessageActionResult

Common Patterns

Notify on long-task completion

python
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

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

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

python
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

python
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

python
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