โ† Back to Communication
Communication by @tmchow

telegram-compose

Compose rich, readable Telegram messages using HTML

0
Source Code

Telegram Compose

Format and deliver rich, scannable Telegram messages via direct API with HTML formatting.

How This Skill Gets Used

This skill is auto-invoked by the main session agent. No other skills need to know about it.

Decision Rule (for the main session agent)

Before sending a message to Telegram, check:

  • Short reply (<3 lines, no structure): Send directly via OpenClaw message tool. Done.
  • Substantive content (>3 lines, or has lists/stats/sections/reports): Spawn this skill as a sub-agent.

Spawning the sub-agent

The main session agent calls sessions_spawn with:

sessions_spawn(
  model: "claude-haiku-4-5",
  task: "<task content โ€” see template below>"
)

Task template:

Read the telegram-compose skill at {baseDir}/SKILL.md for formatting rules, then format and send this content to Telegram.

Bot account: <account_name>  (e.g., "main" โ€” must match a key in channels.telegram.accounts)
Chat ID: <chat_id>
Thread ID: <thread_id>  (omit this line if not a forum/topic chat)

Content to format:
---
<raw content here>
---

After sending, reply with the message_id on success or the error on failure. Do NOT include the formatted message in your reply โ€” it's already been sent to Telegram.

IMPORTANT: The caller MUST specify which bot account to use. The sub-agent must NOT auto-select or iterate accounts.

CRITICAL: The sub-agent announcement routes back to the main session, NOT to Telegram. So the main session should reply NO_REPLY after spawning to avoid double-messaging. The sub-agent's curl call is what delivers to Telegram.

What the sub-agent receives

  1. Skill path โ€” so it can read the formatting rules
  2. Bot account name โ€” which Telegram bot account to use (must be specified, never auto-selected)
  3. Chat ID โ€” where to send
  4. Thread ID โ€” topic thread if applicable
  5. Raw content โ€” the unformatted text/data to turn into a rich message

Credentials

Bot token: Stored in the OpenClaw config file under channels.telegram.accounts.<name>.botToken.

The account name is always provided by the caller. Never auto-select or iterate accounts.

# Auto-detect config path
CONFIG=$([ -f ~/.openclaw/openclaw.json ] && echo ~/.openclaw/openclaw.json || echo ~/.openclaw/clawdbot.json)

# ACCOUNT is provided by the caller (e.g., "main")
# Validate the account exists before extracting the token
ACCOUNT="<provided_account_name>"
BOT_TOKEN=$(jq -r ".channels.telegram.accounts.$ACCOUNT.botToken" "$CONFIG")

if [ "$BOT_TOKEN" = "null" ] || [ -z "$BOT_TOKEN" ]; then
  echo "ERROR: Account '$ACCOUNT' not found in config or has no botToken"
  exit 1
fi

Sending

CONFIG=$([ -f ~/.openclaw/openclaw.json ] && echo ~/.openclaw/openclaw.json || echo ~/.openclaw/clawdbot.json)
# ACCOUNT provided by caller โ€” never auto-select
BOT_TOKEN=$(jq -r ".channels.telegram.accounts.$ACCOUNT.botToken" "$CONFIG")

# Without topic thread
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
  -H "Content-Type: application/json" \
  -d "$(jq -n \
    --arg chat "$CHAT_ID" \
    --arg text "$MESSAGE" \
    '{
      chat_id: $chat,
      text: $text,
      parse_mode: "HTML",
      link_preview_options: { is_disabled: true }
    }')"

# With topic thread
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
  -H "Content-Type: application/json" \
  -d "$(jq -n \
    --arg chat "$CHAT_ID" \
    --arg text "$MESSAGE" \
    --argjson thread $THREAD_ID \
    '{
      chat_id: $chat,
      text: $text,
      parse_mode: "HTML",
      message_thread_id: $thread,
      link_preview_options: { is_disabled: true }
    }')"

Formatting Rules

HTML Tags

<b>bold</b>  <i>italic</i>  <u>underline</u>  <s>strike</s>
<code>mono</code>  <pre>code block</pre>
<tg-spoiler>hidden until tapped</tg-spoiler>
<blockquote>quote</blockquote>
<blockquote expandable>collapsed by default</blockquote>
<a href="url">link</a>
<a href="tg://user?id=123">mention by ID</a>

Escaping

Escape these characters in text content only (not in your HTML tags):

  • & โ†’ &amp; (do this FIRST to avoid double-escaping)
  • < โ†’ &lt;
  • > โ†’ &gt;

Common gotcha: content containing & (e.g., "R&D", "Q&A") will break HTML parsing if not escaped.

Structure Pattern

EMOJI <b>HEADING IN CAPS</b>

<b>Label:</b> Value
<b>Label:</b> Value

<b>SECTION</b>

โ€ข Bullet point
โ€ข Another point

<blockquote>Key quote or summary</blockquote>

<blockquote expandable><b>Details</b>

Hidden content here...
Long details go in expandable blocks.</blockquote>

<a href="https://...">Action Link โ†’</a>

Style Rules

  1. Faux headings: EMOJI <b>CAPS TITLE</b> with blank line after
  2. Emojis: 1-3 per message as visual anchors, not decoration
  3. Whitespace: Blank lines between sections
  4. Long content: Use <blockquote expandable>
  5. Links: Own line, with arrow: Link Text โ†’

Examples

Status update:

๐Ÿ“‹ <b>TASK COMPLETE</b>

<b>Task:</b> Deploy v2.3
<b>Status:</b> โœ… Done
<b>Duration:</b> 12 min

<blockquote>All health checks passing.</blockquote>

Alert:

โš ๏ธ <b>ATTENTION NEEDED</b>

<b>Issue:</b> API rate limit at 90%
<b>Action:</b> Review usage

<a href="https://dashboard.example.com">View Dashboard โ†’</a>

List:

โœ… <b>PRIORITIES</b>

โ€ข <s>Review PR #234</s> โ€” done
โ€ข <b>Finish docs</b> โ€” in progress
โ€ข Deploy staging

<i>2 of 3 complete</i>

Mobile-Friendly Data Display

Never use <pre> for stats, summaries, or visual layouts. <pre> uses monospace font and wraps badly on mobile, breaking alignment and tree characters. Reserve <pre> for actual code/commands only.

For structured data, use emoji + bold + separators:

โŒ BAD (wraps on mobile):
<pre>
โ”œโ”€ ๐ŸŸ  Reddit  32 threads โ”‚ 1,658 pts
โ””โ”€ ๐ŸŒ Web     8 pages
</pre>

โœ… GOOD (flows naturally):
๐ŸŸ  <b>Reddit:</b> 32 threads ยท 1,658 pts ยท 625 comments
๐Ÿ”ต <b>X:</b> 22 posts ยท 10,695 likes ยท 1,137 reposts
๐ŸŒ <b>Web:</b> 8 pages (supplementary)
๐Ÿ—ฃ๏ธ <b>Top voices:</b> @handle1 ยท @handle2 ยท r/subreddit

Other patterns:

Record cards:

<b>Ruby</b>
Birthday: Jun 16 ยท Age: 11

<b>Rhodes</b>
Birthday: Oct 1 ยท Age: 8

Bullet lists:

โ€ข <b>hzl-cli:</b> 1.12.0
โ€ข <b>skill:</b> 1.0.6

Limits and Splitting

  • Message max: 4,096 characters
  • Caption max: 1,024 characters

If formatted message exceeds 4,096 chars:

  1. Split at section boundaries (blank lines between <b>HEADING</b> blocks)
  2. Each chunk must be valid HTML (don't split inside a tag)
  3. Send chunks sequentially with a 1-second delay between them
  4. First chunk gets the full heading; subsequent chunks get a continuation indicator: <i>(continued)</i>

Error Handling

If Telegram API returns an error:

Error Action
Bad Request: can't parse entities HTML is malformed. Strip all HTML tags and resend as plain text.
Bad Request: message is too long Split per the rules above and retry.
Bad Request: message thread not found Retry without message_thread_id (sends to General).
Too Many Requests: retry after X Wait X seconds, then retry once.
Any other error Report the error back; don't retry.

Fallback rule: If HTML formatting fails twice, send as plain text rather than not sending at all. Delivery matters more than formatting.


Sub-Agent Execution Checklist

When running as a sub-agent, follow this sequence:

  1. Parse the task โ€” extract Bot account name, Chat ID, Thread ID (if any), skill path, and raw content
  2. Read this SKILL.md โ€” load the formatting rules
  3. Format the content โ€” apply HTML tags, structure pattern, style rules, mobile-friendly data display
  4. Escape special chars โ€” & then < then > in text content only (not in your HTML tags)
  5. Check length โ€” if >4,096 chars, split at section boundaries
  6. Get bot token โ€” auto-detect config path, extract token for the specified account (error if not found)
  7. Send via curl โ€” use the appropriate template (with/without thread ID)
  8. Check response โ€” parse curl output for "ok": true
  9. Handle errors โ€” follow the error handling table above
  10. Report back โ€” reply with message_id on success, or error details on failure