A Telegram bot that turns links into Notion pages. DM it any URL and the right thing happens automatically.
- Instagram or TikTok cooking reel — transcribes the audio with Whisper, pulls the caption, generates a clean written recipe (ingredients, steps, cuisine, time), saves it as a row in a
recipesdatabase in Notion. - Any other link (article, YouTube, tweet) — fetches the content, summarises it with Sonnet, saves it as a child page under an
Agent's Notesparent page.
You can append free-text instructions after the URL and the bot honours them: save as recipe, this is Italian, skip the summary.
Runs headlessly via macOS LaunchAgent. Wrapped in caffeinate so sleep doesn't kill it. Uses your Claude Code subscription — no extra API spend.
What you'll need
- Mac with Homebrew installed
- Claude Code CLI installed and authenticated (
claude --versionshould work) - Telegram account
- Notion account (free tier is fine)
Part 1 — Manual prep
These steps need a human. Do them first.
Step 1 — Create the Telegram bot
- Open Telegram, search @BotFather, send
/newbot - Pick a name (e.g. "Note Taking") and a username ending in
_bot - Copy the bot token — looks like
1234567890:AAGxxxx.... Save for later.
Step 2 — DM the bot
Find your new bot in Telegram and send any message (e.g. hi). Required so the bot can find your chat.
Step 3 — Create a Notion integration
- Go to https://www.notion.so/profile/integrations
- Click New integration. Name it
note_taking_bot. Pick the workspace. - Configure → Capabilities → tick Read content, Update content, Insert content. Save.
- Copy the Internal Integration Secret (starts with
ntn_).
Step 4 — Create the Notion home page
- In Notion, create a new top-level page called Agent's Notes.
- Click the page's ⋯ → Connections → add your
note_taking_botintegration. - Copy the page URL (Share → Copy link).
The recipes database underneath gets created automatically by the setup prompt in Step 5.
Part 2 — Automated setup
You need three values:
- BOT_TOKEN from BotFather (Part 1 Step 1)
- NOTION_TOKEN from your integration (Part 1 Step 3)
- AGENT_NOTES_URL of the Agent's Notes page (Part 1 Step 4)
CHAT_ID gets auto-resolved from getUpdates once you've DM'd the bot.
Open Claude Code in your terminal
claudePaste this prompt (fill in the three values at the top)
I want to set up a personal note-taking Telegram bot. When I DM it a link, it
saves the content to Notion. Recipes from Instagram/TikTok go in a recipes
database. Everything else becomes a child page of an "Agent's Notes" parent
page. Headless, no tmux, no inline buttons. Caffeinated LaunchAgent so it
survives sleep.
My values:
BOT_TOKEN=PASTE_YOUR_BOT_TOKEN_HERE
NOTION_TOKEN=PASTE_YOUR_NOTION_TOKEN_HERE
AGENT_NOTES_URL=PASTE_YOUR_AGENT_NOTES_URL_HERE
Execute every step in order. Show me the output of each verification command
before moving on. Stop and ask me if anything fails.
================================================================
STEP 1 — Sanity check
================================================================
Run:
claude --version
which claude node
uname -m
brew --prefix
If claude or node is missing, stop and tell me to install them first.
Note arm64 (brew prefix /opt/homebrew) vs x86_64 (/usr/local) — the
LaunchAgent in STEP 13 needs the correct PATH.
================================================================
STEP 2 — Install media tools
================================================================
brew install yt-dlp ffmpeg
Apple Silicon only:
pip3 install --user --break-system-packages mlx-whisper
Verify:
yt-dlp --version
ffmpeg -version | head -1
python3 -c "import mlx_whisper; print('whisper OK')" # Apple Silicon only
================================================================
STEP 3 — Resolve my Telegram chat_id
================================================================
The bot must already have a DM from me (Part 1 Step 2). Run:
curl -s "https://api.telegram.org/bot${BOT_TOKEN}/getUpdates" \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
for u in d.get('result', []):
msg = u.get('message') or u.get('edited_message') or {}
chat = msg.get('chat', {})
if chat.get('type') == 'private':
print(chat['id']); break
"
Store the printed number as CHAT_ID. If nothing prints, stop and tell me
to DM the bot first.
================================================================
STEP 4 — Resolve Agent's Notes page id
================================================================
Extract the 32-hex page id from the end of AGENT_NOTES_URL. Format with
dashes (8-4-4-4-12). Store as AGENT_NOTES_ID.
================================================================
STEP 5 — Create the recipes database under Agent's Notes
================================================================
POST to https://api.notion.com/v1/databases:
curl -s -X POST -H "Authorization: Bearer ${NOTION_TOKEN}" \
-H "Notion-Version: 2022-06-28" -H "Content-Type: application/json" \
"https://api.notion.com/v1/databases" \
-d '{
"parent": {"type":"page_id","page_id":"AGENT_NOTES_ID_HERE"},
"title": [{"type":"text","text":{"content":"recipes"}}],
"is_inline": true,
"properties": {
"Meal Name": {"title": {}},
"Cuisine": {"multi_select": {"options":[
{"name":"Italian"},{"name":"Indian"},{"name":"Chinese"},
{"name":"Thai"},{"name":"Korean"},{"name":"Japanese"},
{"name":"Vietnamese"},{"name":"Mexican"},{"name":"Mediterranean"},
{"name":"Middle Eastern"},{"name":"French"},{"name":"Western"}
]}},
"Time it takes": {"number": {}},
"Created time": {"created_time": {}}
}
}' \
| python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('id') or d)"
Capture the returned id as RECIPES_DB_ID. If you get 401, stop —
the integration isn't shared with the Agent's Notes page; tell me to
fix it via ⋯ → Connections.
================================================================
STEP 6 — Create directory layout
================================================================
mkdir -p ~/.claude/telegram/note_taking
mkdir -p ~/.claude/agents ~/.claude/memory ~/.claude/logs ~/.claude/tools
================================================================
STEP 7 — Credentials, chat id, offset
================================================================
Create ~/.claude/telegram/.env (mode 600). If the file exists, append to
it; if it doesn't, create it.
NOTE_TAKING_BOT_TOKEN="<BOT_TOKEN>"
Then:
chmod 600 ~/.claude/telegram/.env
echo "<CHAT_ID>" > ~/.claude/telegram/note_taking/chat_id.txt
echo "0" > ~/.claude/telegram/note_taking/offset.txt
touch ~/.claude/telegram/note_taking/history.txt
================================================================
STEP 8 — Write the worker script
================================================================
Create ~/.claude/tools/note-taking-save.py with the EXACT content from
the file referenced in this guide. After writing, replace the three CONFIG
constants near the top with the values resolved earlier:
- NOTION_TOKEN → my NOTION_TOKEN
- RECIPES_DB_ID → from STEP 5
- AGENT_NOTES_PAGE_ID → from STEP 4
The full content of note-taking-save.py is reproduced verbatim in
"Appendix A — note-taking-save.py" at the bottom of this guide. Copy it.
================================================================
STEP 9 — Write the agent persona
================================================================
Create ~/.claude/agents/note_taking.md with this content:
# Note Taking Bot
You are a note-taking assistant. You receive messages via Telegram. Your
job is to save links to Notion — recipes in the recipes DB, anything else
as a child page under "Agent's Notes".
URL handling is automated by loop.sh — by the time you're invoked, the
save has already happened (or failed). You only get called when the
message contains NO URL. In that case respond conversationally.
Keep replies short and mobile-friendly. No emoji. No filler.
================================================================
STEP 10 — Write receive.sh, send.sh, loop.sh
================================================================
Create the three bot scripts in ~/.claude/telegram/note_taking/. Their
exact content is reproduced verbatim in "Appendix B — bot scripts" at
the bottom of this guide. Copy each one.
================================================================
STEP 11 — Write the LaunchAgent plist
================================================================
Determine USERNAME (from `whoami`) and BREW_PREFIX (from STEP 1).
Create ~/Library/LaunchAgents/com.claude.telegram.note_taking.plist with:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.claude.telegram.note_taking</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/caffeinate</string>
<string>-dimsu</string>
<string>/bin/bash</string>
<string>/Users/USERNAME/.claude/telegram/note_taking/loop.sh</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>BREW_PREFIX:/usr/local/bin:/usr/bin:/bin</string>
</dict>
<key>WorkingDirectory</key>
<string>/Users/USERNAME/.claude</string>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>ProcessType</key><string>Interactive</string>
<key>StandardOutPath</key>
<string>/Users/USERNAME/.claude/logs/telegram-note_taking.log</string>
<key>StandardErrorPath</key>
<string>/Users/USERNAME/.claude/logs/telegram-note_taking.error.log</string>
</dict>
</plist>
================================================================
STEP 12 — Load and verify
================================================================
launchctl unload ~/Library/LaunchAgents/com.claude.telegram.note_taking.plist 2>/dev/null
launchctl load ~/Library/LaunchAgents/com.claude.telegram.note_taking.plist
sleep 3
launchctl list | grep com.claude.telegram.note_taking
tail -20 ~/.claude/logs/telegram-note_taking.log
The launchctl line should show a PID (not "-"). Log should say
"note_taking bot starting".
================================================================
STEP 13 — Sleep / lock survival (optional but recommended)
================================================================
Ask me first — system-wide power setting:
sudo pmset -c sleep 0 disksleep 0 powernap 1 tcpkeepalive 1
Stops the Mac sleeping on AC power. The caffeinate wrapper in the plist
handles the rest.
================================================================
STEP 14 — Smoke test
================================================================
DM the bot from your phone with:
- A TikTok or Instagram cooking reel URL → recipe row in the recipes DB.
- An article URL → child page under Agent's Notes with a summary.
Tail the log live:
tail -f ~/.claude/logs/telegram-note_taking.log
Tell me what you see.Part 3 — How it routes
The bot decides recipe vs note like this:
- If your text after the link explicitly says "recipe" or "note", that wins.
- Otherwise: recipe if the content describes food preparation (ingredients + cooking steps); note otherwise.
For Instagram and TikTok recipes, the spoken transcript is often vague ("chuck a bit of garlic in"). The caption usually has the real ingredient quantities. The bot reads both and prefers the caption for ingredients, the transcript for technique cues.
Cuisine is a multi-select with 12 options: Italian, Indian, Chinese, Thai, Korean, Japanese, Vietnamese, Mexican, Mediterranean, Middle Eastern, French, Western. Sonnet picks one or two that fit.
Time is an integer estimate in minutes (start to finish).
Part 4 — Day to day
- Just paste a link. The bot replies with the Notion URL once saved.
- Add notes after the link for explicit overrides:
<url> save as recipe,<url> Italian, 45 min,<url> just keep the source — no summary. - Conversation without a link. Ask the bot anything; it falls back to a Sonnet conversational reply.
Part 5 — Troubleshooting
Bot doesn't reply
launchctl list | grep com.claude.telegram.note_takingPID column should be a number. If -, the loop crashed. Check logs:
tail -50 ~/.claude/logs/telegram-note_taking.log
tail -50 ~/.claude/logs/telegram-note_taking.error.logNotion saves return NOTION_ERROR: 401
The Agent's Notes page or recipes DB isn't shared with your integration. Open in Notion → ⋯ → Connections → add it.
Recipes fail with validation_error on Cuisine
Sonnet picked a cuisine that isn't in your dropdown. Edit note-taking-save.py and add it to CUISINE_OPTIONS, or rerun with explicit cuisine in the message.
Whisper crashes on Apple Silicon
pip3 install --user --break-system-packages --upgrade mlx-whisperOn Intel Macs, swap mlx_whisper for the OpenAI whisper package — slower but works.
Bot stalls when Mac sleeps
The plist already wraps caffeinate -dimsu. If it still happens, run STEP 13.
Bot stops on logout
LaunchAgents stop on full user logout (not lock screen, not sleep). To survive logout you need a LaunchDaemon at /Library/LaunchDaemons/ — separate setup, ask if you need it.
Appendix A — note-taking-save.py
The full content of ~/.claude/tools/note-taking-save.py. Copy verbatim. Replace the three CONFIG constants near the top with your values from setup steps 4 and 5.
[ See contents at: ~/.claude/tools/note-taking-save.py
— copy that file verbatim into the new install ]Appendix B — bot scripts
The three small scripts live in ~/.claude/telegram/note_taking/. Copy verbatim.
receive.sh
[ See contents at: ~/.claude/telegram/note_taking/receive.sh ]send.sh
[ See contents at: ~/.claude/telegram/note_taking/send.sh ]loop.sh
[ See contents at: ~/.claude/telegram/note_taking/loop.sh ]Notes on auto-publishing this guide
The Notion API auto-publish failed repeatedly (Cloudflare WAF blocks payloads containing inline source code). Three options for getting it published:
- Open this file and paste into Notion manually — fastest. Each
[ See contents at: ... ]placeholder needs you to copy the actual file contents from disk into the Notion code block. - Host the scripts as GitHub gists and replace the placeholders with gist URLs — cleanest for sharing publicly.
- Retry auto-publish with much smaller chunks and longer delays — possible but unreliable.