Sending & receiving
Handlers receive a Thread and a Message (or Action). thread.post answers the inbound message that started this turn. It enqueues a command; the adapter sends it.
The message
| Field | What it is |
|---|---|
text | Plain text. Empty when the channel withheld content (e.g. Discord without Message Content Intent). |
thread_id | Stable id, e.g. telegram:…. |
chat_kind | dm / group / channel. |
sender | Who sent it. |
message_id | Platform message id — for reply, react, pin, delete, edit. |
attachments | Tuple of Attachment (photo, file, …). |
reply_to | Quoted / replied-to message id, if any. |
raw | Platform payload for power users. |
post, send, and reply
Three different ways to put text on the thread. The Telegram example exposes each as a slash command (/send, /reply; everything else uses post).
| Method | What it does |
|---|---|
thread.post(text) |
Answer the inbound message that started this turn. On email this is a threaded reply (In-Reply-To / References). On Telegram/Slack it replies in the same chat/thread. |
thread.send(text) |
A new message that is not a reply to the inbound turn. On email: no In-Reply-To, so it starts its own conversation. On Telegram: a standalone post in the chat (the example’s /send). |
thread.reply(message_id, text) |
Quote / reply-to a specific platform message id (Telegram quote, Slack thread). Pass msg.message_id for the message that just arrived. |
thread.post("answers this turn")
# email: threaded reply (In-Reply-To). Telegram/Slack: same chat.
thread.send("standalone — not a reply to your message")
# new conversation on email; not attached to the inbound turn
thread.reply(msg.message_id, "quoted.")
# reply-to that specific message id (Telegram quote)
thread.post("with buttons", actions=(
Button(label="ok", data="ok"),
Button(label="docs", url="https://example.com"),
))await thread.post("answers this turn")
await thread.send("standalone — not a reply to your message")
await thread.reply(msg.message_id, "quoted.")
await thread.post("with buttons", {
actions: [
{ label: "ok", data: "ok" },
{ label: "docs", url: "https://example.com" },
],
})Turn-taking
| Method | |
|---|---|
typing() | Typing indicator where the channel has one. |
edit(message_id, text) | Edit a sent message. |
delete(message_id) | Delete. |
react(message_id, emoji) | Add a reaction. |
pin / unpin | Pin in groups (Telegram pin in DMs is a no-op with a hint). |
forward(to_thread_id, message_id) | Forward. |
send_media(attachment, caption=…) | Photo/file by URL or adapter attachment. |
send_blocks(blocks, text=…) | Rich layout; adapters degrade (Telegram → text + keyboard). |
initiate(text) | Cold-DM shape; maps to send on channels that allow it. |
schedule(text, send_at) | Send later. |
Need a native method the core does not name (Telegram sendPhoto with extra flags)? Call the adapter / caspian catalog get telegram.send-photo. The bot program never names sendMessage. TypeScript thread methods are camelCase (sendMedia, sendBlocks, markRead); snake_case aliases exist.
Acknowledgement (ack)
ack is an acknowledgement: an instant reply sent before the handler runs, so the human sees a line while the model is still thinking. Channels with no typing indicator (email, SMS, X) need this; Discord/Telegram can use thread.typing() instead.
This is not the same as the adapter acknowledging a button press (that always happens so the spinner stops). ack is a user-visible message on the thread.
@cx.on_message({"channel": "email", "ack": "On it…"})
def handle(thread, msg, ctx):
thread.post(slow_answer(msg.text))
# human already got "On it…" ; this post is the real answercx.onMessage({ channel: "email", ack: "On it…" }, async (thread, msg) => {
await thread.post(await slowAnswer(msg.text))
// human already got "On it…" ; this post is the real answer
})