All guides

Build a Note-Taking Telegram Bot for Claude Code

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 recipes database 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 Notes parent 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 --version should 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

  1. Open Telegram, search @BotFather, send /newbot
  2. Pick a name (e.g. "Note Taking") and a username ending in _bot
  3. 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

  1. Go to https://www.notion.so/profile/integrations
  2. Click New integration. Name it note_taking_bot. Pick the workspace.
  3. Configure → Capabilities → tick Read content, Update content, Insert content. Save.
  4. Copy the Internal Integration Secret (starts with ntn_).

Step 4 — Create the Notion home page

  1. In Notion, create a new top-level page called Agent's Notes.
  2. Click the page's Connections → add your note_taking_bot integration.
  3. 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

code
claude

Paste this prompt (fill in the three values at the top)

code
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:

  1. If your text after the link explicitly says "recipe" or "note", that wins.
  2. 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

bash
launchctl list | grep com.claude.telegram.note_taking

PID column should be a number. If -, the loop crashed. Check logs:

bash
tail -50 ~/.claude/logs/telegram-note_taking.log
tail -50 ~/.claude/logs/telegram-note_taking.error.log

Notion 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

bash
pip3 install --user --break-system-packages --upgrade mlx-whisper

On 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.

python
[ 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

bash
[ See contents at: ~/.claude/telegram/note_taking/receive.sh ]

send.sh

bash
[ See contents at: ~/.claude/telegram/note_taking/send.sh ]

loop.sh

bash
[ 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:

  1. 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.
  2. Host the scripts as GitHub gists and replace the placeholders with gist URLs — cleanest for sharing publicly.
  3. Retry auto-publish with much smaller chunks and longer delays — possible but unreliable.

Learn this inside the community

The full course, templates, and the people building this, free in the Skool community.

Join the community