Telegram Compose
Advertencia de seguridad

Format and deliver rich Telegram messages with HTML formatting via direct Telegram API. Auto-invoked by the main session for substantive Telegram output — no other skills need to call it. Decision rule: If your Telegram reply is >3 lines or contains structured data (lists, stats, sections, reports), spawn this as a Haiku sub-agent to format and send. Short replies (<3 lines) go directly via OpenClaw message tool. Handles: research summaries, alerts, status updates, reports, briefings, notifications — anything with visual hierarchy.

Instalar
$clawhub install telegram-compose

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: ``` 📋 TASK COMPLETE

Task: Deploy v2.3 Status: ✅ Done Duration: 12 min

All health checks passing.

**Alert:**

⚠️ ATTENTION NEEDED

Issue: API rate limit at 90% Action: Review usage

View Dashboard → ```

List: ``` ✅ PRIORITIES

Review PR #234 — done • Finish docs — in progress • Deploy staging

2 of 3 complete ```


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: ``` Ruby Birthday: Jun 16 · Age: 11

Rhodes 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