No work step here. This is probably a skill that reads or coordinates, not one that produces something.
.md file to compare - side-by-side diff against telegram
telegram
description: "Triggers on prompt mention of 'telegram'."
What it does for you
Reads and sends your Telegram messages.
What it produces
A recent result, so you can see the kind of work it returns.
loading…
How to get it
These run inside the Snappy workspace. Want this working in your business? I set skills like this up with you, in one focused week.
For developers how this skill is built, graded, and how it runs
at a glance- the short version
what's inside - the parts that make up a skill 3/4 present
A skill is just a few plain-text files. Only the main one is required. The rest are optional, added as the work needs them. This is what the skill is made of; how it runs is just below.
state/skills/telegram/SKILL.md
present
state/lib/telegram.ts
present
state/bin/telegram/
not present
state/skills/telegram/AGENTS.md
present
how it runs - the shared frame every skill uses 3/5 present
Every skill runs the same way. One part does the work, a separate part checks it, and a short loader hands the AI exactly what it needs for the job. Anything this skill doesn't use shows a one-line note saying why, on purpose, not by accident.
state/log/evals.ndjson what it has learned - fixes written back in over time sample
When a run hits something this skill didn't handle, the fix gets written back into the skill so it doesn't happen again. FIXED means it was corrected on the spot. LOGGED means it's queued for a bigger rewrite. Either way, the skill gets a little better and never makes the same mistake twice.
- Loading feedback rows…
SKILL.md- the skill, written out in plain English
telegram
Telegram Bot API wrapper backed by state/lib/telegram.ts.
The OpenUI resource at state/skills/telegram/resources/ui.openui uses Query("get_inbox_telegram") to render recent Telegram messages for the connected Robert chat.
What it's for
- Telegram reads. Inspect recent messages with
getRecentTelegramMessages()
or the /inbox/telegram endpoint.
- Telegram sends. Send text, photos, documents, or chat actions when an
upstream skill has explicit permission to apply.
- Generated component surface. The skill-owned OpenUI resource is the
user-facing Telegram view. Improve that resource when the Telegram work surface needs richer interaction or clearer presentation.
When NOT to use it
- Multi-channel sweeps. Use
sweepwhen Telegram is one input among Slack,
email, LinkedIn, Skool, or other channels.
- Implicit sends. Default to scope-only. Do not send Telegram messages
unless the caller explicitly requested an applied send.
Steps
- For Telegram-only reads, call
getRecentTelegramMessages()from
state/lib/telegram.ts or inspect /inbox/telegram.
- For Telegram sends, call
sendText,sendPhoto, orsendDocumentonly
when the request is explicitly apply-mode.
- Before rendering or editing the Telegram generated component surface, read
state/skills/telegram/resources/ui.openui.
- Write one eval row for real runs.
Library API
import {
getRecentTelegramMessages,
sendText,
sendPhoto,
sendDocument,
} from "./state/lib/telegram.ts";
Credentials read from .env.cache: TELEGRAM_BOT_TOKEN and TELEGRAM_ROBERT_CHAT_ID.
Eval
Kind: auto-shape. Frontmatter plus paired loader/resource presence is the structural gate. Behavioral send evals require explicit applied requests.
Files
state/lib/telegram.ts- importable and CLI-runnable Telegram API.state/skills/telegram/resources/ui.openui- skill-owned OpenUI resource.
AGENTS.md- what the AI loads when this skill comes up
telegram - loader
Per-turn rules. Full reference: state/skills/telegram/SKILL.md. Lib: state/lib/telegram.ts.
Critical Rules
- Scope-only by default. Reading is safe; sending text, photos, or documents
requires an explicit applied-send request.
- Credentials gate.
TELEGRAM_BOT_TOKENandTELEGRAM_ROBERT_CHAT_IDmust
be present in .env.cache. Surface missing credentials directly.
- Use
sweepfor multi-channel work. Use this skill for Telegram-only
reads or sends.
- Generated UI is skill-owned. The OpenUI resource binds to
get_inbox_telegram; do not inline raw Telegram data into a parallel global component.
OpenUI Resource
- Skill-owned OpenUI Lang resource:
state/skills/telegram/resources/ui.openui. Read it before rendering or editing this skill's generated component surface. - Treat this resource as a first-class artifact of the skill, not a generic chat response. Improve it when the skill's user-facing output needs to become richer.
- System resources compose OpenUI primitives and inherit SnappyChat tokens. Use
ui_contract: brandedin SKILL.md only for deliberate platform or client visuals.
Commands
| operation | command |
|---|---|
| read recent messages | import { getRecentTelegramMessages } from "./state/lib/telegram.ts" |
| send text | import { sendText } from "./state/lib/telegram.ts" |
| send photo | import { sendPhoto } from "./state/lib/telegram.ts" |
| send document | import { sendDocument } from "./state/lib/telegram.ts" |
| CLI smoke | npx tsx state/lib/telegram.ts |
| credentials | .env.cache -> TELEGRAM_BOT_TOKEN, TELEGRAM_ROBERT_CHAT_ID |
| OpenUI resource | state/skills/telegram/resources/ui.openui |
Self-Test
After reading this loader:
- [ ] Route Telegram-only reads to
state/lib/telegram.ts. - [ ] Refuse implicit sends unless apply-mode is explicit.
- [ ] Read
state/skills/telegram/resources/ui.openuibefore rendering or editing
the generated Telegram surface.
- [ ] Check Telegram credentials before any API call.
Self-correcting loader (PID feedback)
If this loader did not cover your case, attempt a small inline edit first.
echo "[$(date -u +%FT%TZ)] telegram: <what was missing or fixed> [FIXED|LOGGED] action_kind=skill-ran" >> state/log/loader-feedback.logapi.ts- the code it can call
#!/usr/bin/env npx tsx
/**
* snappy-telegram/api.ts -- Telegram Bot API operations for all snappy-* skills.
*
* Uses Telegram Bot Token from snappy-settings/.env.cache.
* Direct Telegram Bot API calls.
*
* Usage:
* npx tsx api.ts send "Hello from the agent"
* npx tsx api.ts send "Hello" --parse-mode HTML
* npx tsx api.ts photo "https://example.com/img.jpg"
*
* Or import as module:
* import { sendText, sendPhoto, editTelegramMessage, deleteTelegramMessage, getRecentTelegramMessages } from "./telegram.ts";
*/
import { env } from "./env.ts";
import { realpathSync } from "fs";
const BASE = () => `https://api.telegram.org/bot${env("TELEGRAM_BOT_TOKEN")}`;
const ROBERT_CHAT_ID = () => env("TELEGRAM_ROBERT_CHAT_ID");
async function tg(method: string, body: Record<string, unknown>) {
const res = await fetch(`${BASE()}/${method}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await res.json();
if (!data.ok) {
throw new Error(`Telegram ${method} failed: ${data.description}`);
}
return data.result;
}
// --- Public API ---
export async function sendText(
text: string,
parseMode: "Markdown" | "HTML" = "Markdown",
chatId?: string,
) {
return tg("sendMessage", {
chat_id: chatId || ROBERT_CHAT_ID(),
text,
parse_mode: parseMode,
});
}
export async function sendPhoto(photoUrl: string, caption?: string, chatId?: string) {
return tg("sendPhoto", {
chat_id: chatId || ROBERT_CHAT_ID(),
photo: photoUrl,
...(caption ? { caption, parse_mode: "Markdown" } : {}),
});
}
export async function sendDocument(documentUrl: string, caption?: string, chatId?: string) {
return tg("sendDocument", {
chat_id: chatId || ROBERT_CHAT_ID(),
document: documentUrl,
...(caption ? { caption } : {}),
});
}
export async function editTelegramMessage(
messageId: number,
text: string,
parseMode: "Markdown" | "HTML" = "Markdown",
chatId?: string,
) {
return tg("editMessageText", {
chat_id: chatId || ROBERT_CHAT_ID(),
message_id: messageId,
text,
parse_mode: parseMode,
});
}
export async function sendChatAction(
action: "typing" | "upload_photo" | "upload_document" = "typing",
chatId?: string,
) {
return tg("sendChatAction", {
chat_id: chatId || ROBERT_CHAT_ID(),
action,
});
}
// --- Read operations ---
export async function getUpdates(offset?: number, limit = 100, timeout = 0) {
return tg("getUpdates", {
...(offset !== undefined ? { offset } : {}),
limit,
timeout,
allowed_updates: ["message", "edited_message", "channel_post"],
});
}
export async function getRecentTelegramMessages(
chatId?: string,
limit = 20,
): Promise<
Array<{
message_id: number;
from: string;
text: string;
date: string;
chat_id: number;
}>
> {
const updates = await getUpdates(undefined, 100, 0);
const targetChat = chatId || ROBERT_CHAT_ID();
const messages = (updates as any[])
.filter((u: any) => u.message && String(u.message.chat.id) === String(targetChat))
.map((u: any) => ({
message_id: u.message.message_id,
from: u.message.from?.first_name || u.message.from?.username || "unknown",
text: u.message.text || u.message.caption || "[non-text]",
date: new Date(u.message.date * 1000).toISOString(),
chat_id: u.message.chat.id,
}))
.slice(-limit);
return messages;
}
export async function deleteTelegramMessage(messageId: number, chatId?: string) {
return tg("deleteMessage", {
chat_id: chatId || ROBERT_CHAT_ID(),
message_id: messageId,
});
}
export async function forwardMessage(messageId: number, toChatId: string, fromChatId?: string) {
return tg("forwardMessage", {
chat_id: toChatId,
from_chat_id: fromChatId || ROBERT_CHAT_ID(),
message_id: messageId,
});
}
export async function getMe() {
return tg("getMe", {});
}
// --- CLI ---
if (
(() => {
try {
return import.meta.url === `file://${realpathSync(process.argv[1])}`;
} catch {
return false;
}
})()
) {
(async () => {
const [, , cmd, ...args] = process.argv;
switch (cmd) {
case "send": {
const parseMode = args.includes("--parse-mode")
? (args[args.indexOf("--parse-mode") + 1] as "Markdown" | "HTML")
: "Markdown";
const text = args
.filter((a, i) => a !== "--parse-mode" && args[i - 1] !== "--parse-mode")
.join(" ");
if (!text) {
console.error("Usage: api.ts send <text> [--parse-mode HTML]");
process.exit(1);
}
const result = await sendText(text, parseMode);
console.log(`sent message_id=${result.message_id}`);
break;
}
case "photo": {
const [url, ...captionParts] = args;
if (!url) {
console.error("Usage: api.ts photo <url> [caption]");
process.exit(1);
}
const result = await sendPhoto(url, captionParts.join(" ") || undefined);
console.log(`sent photo message_id=${result.message_id}`);
break;
}
case "edit": {
const [msgId, ...textParts] = args;
if (!msgId || !textParts.length) {
console.error("Usage: api.ts edit <message_id> <text>");
process.exit(1);
}
await editTelegramMessage(Number(msgId), textParts.join(" "));
console.log("edited");
break;
}
case "read": {
const limit = args[0] ? parseInt(args[0], 10) : 20;
const msgs = await getRecentTelegramMessages(undefined, limit);
if (msgs.length === 0) {
console.log(
"No recent messages (bot must have received messages via /start or direct chat)",
);
} else {
for (const m of msgs) {
console.log(`${m.date}\t${m.from}\t${m.text.slice(0, 300)}`);
}
}
break;
}
case "updates": {
const updates = await getUpdates();
console.log(JSON.stringify(updates, null, 2));
break;
}
case "me": {
const me = await getMe();
console.log(JSON.stringify(me, null, 2));
break;
}
case "delete": {
const [msgId] = args;
if (!msgId) {
console.error("Usage: api.ts delete <message_id>");
process.exit(1);
}
await deleteTelegramMessage(Number(msgId));
console.log("deleted");
break;
}
default:
console.log("Usage: npx tsx api.ts [send|photo|edit|read|updates|me|delete] ...");
}
})();
}
scripts- helper scripts it can run
prose-only skill - 1 inline code block live in SKILL.md above (no state/bin/ sidecar yet).
how we check it- the checks, plus the last 10 runs
no recent runs logged - the eval contract is declared but nothing has been graded yet