This template gets a scheduled Telegram bot from "I have a bot token" to "messages are landing in my chat" in under a minute, driven by Claude Code.
The template's index.ts ships as an HTTP webhook responder — useful if you want a chat bot that replies. The guide below is for the more common case: a scheduled push bot that sends a recurring message (daily weather, daily reminder, daily news digest, anything).
- Human creates a bot via @BotFather on Telegram and copies the token.
- Human tells Claude Code: "Use
templates/telegramBotStarterto build a [topic] bot for this token:…" - Claude does everything below, then pauses and asks the human to DM the bot once.
- Human DMs the bot ("hi" /
/start) and says "go". - Claude grabs the chat ID, fires a test, confirms the message landed.
remix_val(val="templates/telegramBotStarter", name="<descriptive-name>")
Pick a name that describes the bot's job, e.g. daily-nyc-weather, morning-stretch-reminder.
Overwrite the file with an interval-type handler that reads two env vars and sends one message.
Minimal skeleton:
import { Bot } from "https://deno.land/x/grammy@v1.35.0/mod.ts";
const TOKEN = Deno.env.get("TELEGRAM_BOT_TOKEN");
const CHAT_ID = Deno.env.get("TELEGRAM_CHAT_ID");
if (!TOKEN) throw new Error("TELEGRAM_BOT_TOKEN is not set");
if (!CHAT_ID) throw new Error("TELEGRAM_CHAT_ID is not set");
export default async function () {
// Fetch whatever the message needs (weather API, RSS, etc.)
const message = "Hello from your bot";
const bot = new Bot(TOKEN);
await bot.api.sendMessage(CHAT_ID, message, { parse_mode: "Markdown" });
}
For a weather bot, Open-Meteo is a no-API-key source. RSS feeds, public JSON endpoints, and any HTTPS API work too.
Use set_file_type to change index.ts from http to interval, then write_interval_settings to set a cron.
Example daily cron: 0 11 * * *. Cron expressions run in UTC. Pick the UTC time that matches the user's desired local time for the destination's timezone (e.g. 0 11 * * * is 7am EDT / 6am EST).
Verify the token first by making a GET request to Telegram's getMe endpoint at api.telegram.org/bot{token}/getMe. A response with is_bot: true confirms the token.
Then add it as an env var on the val:
- Key:
TELEGRAM_BOT_TOKEN - Value: the token from the user
- Description: the bot's @username
Env var naming convention: use
TELEGRAM_BOT_TOKENandTELEGRAM_CHAT_ID. These exact names.
Steps 2, 3, and 4 (update_file, set_file_type, write_interval_settings, add_env_var) have no dependencies on each other. Issue them in a single parallel tool batch.
Return control with a message like:
Bot is wired up and the cron is armed. DM your bot once (any message, or tap Start) so Telegram registers the chat. Tell me when done and I'll grab your chat ID and fire a test message.
Do not attempt to drive Telegram via UI automation or screenshots — the DM is a one-tap user action.
Make a GET request to Telegram's getUpdates endpoint at api.telegram.org/bot{token}/getUpdates. Parse result[0].message.chat.id (use the most recent update if there are several).
Add another env var on the val:
- Key:
TELEGRAM_CHAT_ID - Value: the chat ID from step 7
- Description: who/where the message goes
Use run_file on index.ts. A return with no error means the message has been sent. Tell the human it should now be in their Telegram.
| Item | Value |
|---|---|
| Telegram client | grammy from deno.land/x |
| Weather source | api.open-meteo.com (no API key) |
| Token env var | TELEGRAM_BOT_TOKEN |
| Chat env var | TELEGRAM_CHAT_ID |
| Cron timezone | UTC (Val Town's interval scheduler runs in UTC) |
| Message format | Markdown via parse_mode: "Markdown" |
- For a private DM chat, the chat ID is the user's Telegram user ID — same across every bot that user has DM'd. If you've already discovered it for one bot, you can reuse it for the next.
- For a group chat, add the bot to the group, send any message, and the chat ID will be a negative number in
getUpdates. - For a channel, add the bot as an admin and post a message; the chat ID will appear in
getUpdatesas well. - Open-Meteo accepts a
timezone=parameter — set it to the destination's IANA timezone so "today" in the daily forecast matches the human's local day.