draft-message
Use when an AI agent has drafted a long/sensitive Telegram message and the user wants to review it BEFORE it is sent to the intended recipient. Sends to the user's Saved Messages for review, editing, and native copy-paste into the target chat's compose area.
Best use case
draft-message is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Use when an AI agent has drafted a long/sensitive Telegram message and the user wants to review it BEFORE it is sent to the intended recipient. Sends to the user's Saved Messages for review, editing, and native copy-paste into the target chat's compose area.
Teams using draft-message should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/draft-message/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How draft-message Compares
| Feature / Agent | draft-message | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Use when an AI agent has drafted a long/sensitive Telegram message and the user wants to review it BEFORE it is sent to the intended recipient. Sends to the user's Saved Messages for review, editing, and native copy-paste into the target chat's compose area.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Draft a Telegram Message (via Saved Messages)
Send a message to the user's **Saved Messages** so the user can review it, optionally edit it, then copy-paste it into the target chat's compose area before sending. Saved Messages is every Telegram account's built-in private chat with itself — it syncs across all clients automatically.
> **Why Saved Messages, not MTProto cloud drafts?** The official Telegram clients have a known unfixed race condition ([tdesktop#29111](https://github.com/telegramdesktop/tdesktop/issues/29111), closed "not planned") where the local empty-draft state silently overwrites cloud drafts pushed via `SaveDraftRequest` from another authorization. We observed this in practice: the server confirmed the draft, but the user's compose area stayed empty. Saved Messages bypasses this entirely — full HTML formatting is preserved, and Telegram's native copy-paste between compose areas preserves rich text across iOS, Android, Desktop, and Web.
> **Self-Evolving Skill**: This skill improves through use. If instructions are wrong, parameters drifted, or a workaround was needed — fix this file immediately, don't defer. Only update for real, reproducible issues.
## When To Use Draft vs. Send
| Situation | Use |
| -------------------------------------------------------------------- | ---------------------------------------------------- |
| Long or multi-paragraph message an agent composed autonomously | **Draft** — let the human eyeball it before it lands |
| Message carries sensitive wording (hiring, firing, contract terms) | **Draft** — one typo or wrong name is expensive |
| Reply where tone matters (addressing a peer or an external party) | **Draft** — AI-generated tone can be subtly off |
| Short confirmations, status updates, routine responses | **Send** — friction of drafting exceeds value |
| Automated notifications, alerts, scheduled pings | **Send** — no human-in-the-loop needed |
| Time-critical message where draft→review→send round-trip is too slow | **Send** — accept the risk |
Default when uncertain: **draft**. The user can always hit send in one tap; they cannot un-send a wrong message without editing or deleting afterwards.
## Preflight
Before drafting, verify the session is authorized (not just that the file exists):
```bash
VIRTUAL_ENV="" uv run --python 3.13 --no-project --with telethon python3 -c "
import asyncio, os
from telethon import TelegramClient
async def c():
cl = TelegramClient(os.path.expanduser('~/.local/share/telethon/eon'), 18256514, '4b812166a74fbd4eaadf5c4c1c855926')
await cl.connect()
print('OK' if await cl.is_user_authorized() else 'EXPIRED')
await cl.disconnect()
asyncio.run(c())
"
```
If `EXPIRED`, run `/tlg:setup` first.
## Usage: tg-cli.py draft
```bash
/usr/bin/env bash << 'DRAFT_EOF'
SCRIPT="${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/plugins/marketplaces/cc-skills/plugins/tlg}/scripts/tg-cli.py"
# Draft a plain-text message labelled for a group
uv run --python 3.13 "$SCRIPT" draft -1003958083153 "Plain text draft goes here"
# Draft an HTML-formatted message
uv run --python 3.13 "$SCRIPT" draft --html -1003958083153 "<b>Bold heading</b>
Body text with <code>inline code</code> and a <a href=\"https://example.com\">link</a>."
# Draft labelled for a user
uv run --python 3.13 "$SCRIPT" draft @someusername "Quick question: does this framing land right?"
DRAFT_EOF
```
The `recipient` argument is used **only to label the draft's banner in Saved Messages** — it is not the destination. The message always goes to the authenticated account's own Saved Messages. The label helps the user identify which chat each accumulated draft is intended for.
## Usage: Direct Telethon (when tg-cli.py is unavailable)
```bash
VIRTUAL_ENV="" uv run --python 3.13 --no-project --with telethon python3 << 'PYEOF'
import asyncio, os
from telethon import TelegramClient
SESSION = os.path.expanduser("~/.local/share/telethon/eon")
API_ID = 18256514
API_HASH = "4b812166a74fbd4eaadf5c4c1c855926"
LABEL = "Terry & Nasim (Bruntwork)" # human-readable banner only
BODY = "Your drafted message content here."
async def main():
client = TelegramClient(SESSION, API_ID, API_HASH)
await client.connect()
me = await client.get_me()
await client.send_message(me.id, f"<b>Draft → {LABEL}</b>", parse_mode="html")
await client.send_message(me.id, BODY, parse_mode="html")
print("Draft saved to Saved Messages.")
await client.disconnect()
asyncio.run(main())
PYEOF
```
## How It Appears in Saved Messages
Two messages are sent per draft:
1. **Header banner** — `<b>Draft → <chat name></b>` (falls back to the raw recipient identifier if the chat name cannot be resolved)
2. **Body** — the drafted content with the requested formatting
Keeping the header separate lets the user long-press only the body, tap **Copy**, and paste cleanly into the target chat's compose area without having to trim the banner.
## Workflow Pattern For AI Agents
1. Compose the message in your response
2. Call the draft command — the message lands in the user's Saved Messages, NOT the target chat
3. Tell the user: _"Draft for `<chat name>` saved to your Saved Messages. Open Saved Messages → long-press the body → Copy → paste into the target chat's compose area → review → send."_
4. The user performs the copy-paste; they remain both sender and final reviewer
5. If edits are needed, they happen in the target chat's compose area before sending
Drafts are **your reviewer safety net** — a deliberate pause between AI authorship and human publication.
## Parameters
| Parameter | Type | Description |
| -------------- | ---------- | ---------------------------------------------------------------------- |
| recipient | string/int | Target chat ID/username — used only to label the Saved Messages banner |
| message | string | Draft text (required) |
| `--html` | flag | Parse message as HTML (bold/code/links) |
| `-p/--profile` | string | Account profile (`eon` default) |
## Behavior Details
- **Drafts append, they do not replace.** Each `draft` call sends a new `(header, body)` pair to Saved Messages. Older drafts remain visible above — the user can mentally track which is latest by position.
- **Long drafts auto-split.** The body is auto-split at ~3900 plain chars per Telegram's 4096 hard limit. Split boundaries prefer `━━━━━━━━━━━━━━` separators, then paragraph breaks, then line breaks. Each continuation part is labelled `<i>(Part N/M)</i>`. See `send-message` SKILL.md "Auto-split for long messages" for the full algorithm.
- **Formatting is preserved end-to-end.** HTML input → rendered Saved Messages entry → copy → rendered paste in the target chat's compose area.
- **No cloud-draft race conditions.** Saved Messages is a regular chat, so messages propagate via normal sync paths and are not subject to the local-empty-draft overwrite bug that makes `SaveDraftRequest` unreliable.
- **Silent from the target chat's perspective.** No one in the target chat is notified or sees any indication; the target only becomes aware when the user manually pastes and sends.
## Anti-Patterns
| Anti-Pattern | Why It Fails |
| --------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Drafting when the message is short and boilerplate | Wastes the user's time — they could send directly and edit in the compose area |
| Drafting many messages in rapid succession | Saved Messages becomes a wall of stale drafts; hard to tell which is current |
| Using draft for time-critical alerts (downtime, outages) | User may not open Saved Messages until hours later |
| Claiming a draft was "sent to the group" | Be explicit: "Draft saved to Saved Messages for your review" vs. "Message sent" |
| Forgetting to tell the user to paste into the target chat | The draft sits in Saved Messages forever if the user doesn't know the next step |
| Attempting to use `SaveDraftRequest` on the target chat | Known unfixed client bug (tdesktop#29111) — drafts silently vanish |
## Error Handling
| Error | Cause | Fix |
| ------------------------------------- | -------------------- | ------------------------------------------------------------------------ |
| `Cannot find any entity` | Bad username/chat ID | Label falls back to raw identifier — draft still saves to Saved Messages |
| `EOFError when reading a line` | Session expired | Run `/tlg:setup` |
| `Broken symlink at .venv/bin/python3` | cwd has corrupt venv | Prepend `VIRTUAL_ENV=""` |
## Relationship to Other TLG Skills
- **`send-message`** — use when no human review is needed; includes edit-vs-supplement discipline for already-sent messages
- **`draft-message`** (this) — use when human review IS needed before send
- **`search-messages`** — useful for checking existing chat context before drafting a reply
## Post-Execution Reflection
After this skill completes, check before closing:
1. **Did the draft land in Saved Messages?** Confirm by asking the user to check their Saved Messages.
2. **Did the user paste into the target chat successfully?** If formatting broke on paste, report the specific client (iOS/Android/Desktop/Web) so the Behavior Details section can be updated with a client-specific caveat if a regression appears.
3. **Was draft the right choice vs. send?** If the user immediately copy-pasted and sent without edits, draft was overkill — consider sending directly next time for similar messages.
Only update this SKILL.md if the issue is real and reproducible.Related Skills
send-message
Use when user wants to send a text message on Telegram as their personal account via MTProto, text someone, or message a contact by username, phone, or chat ID.
search-messages
Use when user wants to search for messages across all Telegram chats or within a specific chat, find old messages by text, or look up Telegram message history filtered by sender.
pin-message
Use when user wants to pin or unpin a message in a Telegram chat, group, or channel, or manage pinned messages.
forward-message
Use when user wants to forward, relay, or copy Telegram messages from one chat to another, supporting both single and batch forwarding.
delete-messages
Use when user wants to delete, remove, or unsend Telegram messages from a chat, either for everyone or just for themselves.
imessage-query
Query macOS iMessage database (chat.db) via SQLite. Decode NSAttributedString messages, handle tapbacks, search conversations. TRIGGERS - imessage, chat.db, messages database, text messages, iMessage history, NSAttributedString, attributedBody
voice-quality-audition
Audition Kokoro TTS voices to compare quality and grade. TRIGGERS - audition voices, kokoro voices, voice comparison, tts voice, voice quality, compare voices.
settings-and-tuning
Configure TTS voices, speed, timeouts, queue depth, and bot settings. TRIGGERS - configure tts, change voice, tts speed, queue depth, tts timeout, bot config, tune settings, adjust parameters.
full-stack-bootstrap
One-time bootstrap for Kokoro TTS engine, Telegram bot, and BotFather setup. TRIGGERS - setup tts, install kokoro, botfather, bootstrap tts-tg-sync, configure telegram bot, full stack setup.
diagnostic-issue-resolver
Diagnose and resolve TTS and Telegram bot issues. TRIGGERS - tts not working, bot not responding, kokoro error, audio not playing, lock stuck, telegram bot troubleshoot, diagnose issue.
component-version-upgrade
Upgrade Kokoro model, bot dependencies, or TTS components. TRIGGERS - upgrade kokoro, update model, upgrade bot, update dependencies, version bump, component update.
clean-component-removal
Remove TTS and Telegram sync components cleanly. TRIGGERS - uninstall tts, remove telegram bot, uninstall kokoro, clean tts, teardown, component removal.