- Published on
Send Notion Updates to Telegram with Cloudflare Workers (2025 Guide)
- Authors
- Name
- Ahmed Farid
- @
TIP
Cloudflare’s free Workers + Cron give you 1 000 000 calls/day—perfect to glue Notion and Telegram without a server.
We’ll create a Cloudflare Worker that runs every 5 minutes:
- Queries a Notion database via REST API.
- Stores the
lastSynced
timestamp in KV. - Filters pages created/updated after that timestamp.
- Sends nicely-formatted messages to a Telegram bot.
- Updates
lastSynced
.
All for free, no external hosts.
Table of Contents
- Table of Contents
- 1. Prerequisites & Terminology
- 2. Create & Configure Worker
- 3. Worker Code
- 4. Deploy & Test
- 5. Rate-Limiting & Error Handling
- 6. Security & Maintenance
- 7. Extending Further
- 8. Conclusion
1. Prerequisites & Terminology
- Cloudflare account (Workers & KV enabled).
- Notion integration created & added to your workspace (with DB read access).
- Telegram bot token & chat ID.
Term | Meaning |
---|---|
Cron Trigger | Cloudflare feature to run worker on schedule |
KV | Global key-value store with eventual consistency |
Filter timestamp | lastSynced ISO value stored in KV |
2. Create & Configure Worker
npm create cloudflare@latest notion-tg
cd notion-tg
Choose TypeScript and KV.
wrangler.toml
:
name = "notion-tg"
main = "src/index.ts"
compatibility_date = "2025-07-30"
[[kv_namespaces]]
binding = "STORE"
id = "<KV_NAMESPACE_ID>"
[[triggers]]
crons = ["*/5 * * * *"]
Add secrets:
wrangler secret put NOTION_TOKEN
wrangler secret put NOTION_DATABASE_ID
wrangler secret put TG_TOKEN
wrangler secret put TG_CHAT_ID
3. Worker Code
src/index.ts
:
import { Env } from './types'
export default {
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
const since = (await env.STORE.get('lastSynced')) ?? '1970-01-01T00:00:00Z'
const notionResp = await fetch(
'https://api.notion.com/v1/databases/' + env.NOTION_DATABASE_ID + '/query',
{
method: 'POST',
headers: {
Authorization: `Bearer ${env.NOTION_TOKEN}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
},
body: JSON.stringify({
filter: {
timestamp: 'last_edited_time',
last_edited_time: { after: since },
},
sorts: [{ timestamp: 'created_time', direction: 'ascending' }],
}),
}
)
const { results } = await notionResp.json<any>()
if (!results.length) return
const messages = results.map((p: any) => {
const title = p.properties.Name?.title?.[0]?.plain_text ?? 'Untitled'
const url = p.url
const edited = p.last_edited_time.split('T')[0]
return `📝 <b>${title}</b>\n${url}\nEdited: ${edited}`
})
await Promise.all(messages.map((m) => sendTelegram(env, m)))
await env.STORE.put('lastSynced', new Date().toISOString())
},
}
async function sendTelegram(env: Env, text: string) {
const url = `https://api.telegram.org/bot${env.TG_TOKEN}/sendMessage`
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: env.TG_CHAT_ID,
text,
parse_mode: 'HTML',
disable_web_page_preview: false,
}),
})
}
src/types.d.ts
:
export interface Env {
NOTION_TOKEN: string
NOTION_DATABASE_ID: string
TG_TOKEN: string
TG_CHAT_ID: string
STORE: KVNamespace
}
4. Deploy & Test
wrangler deploy
Trigger manually:
wrangler tail &
wrangler schedule --cron "*/1 * * * *" --env production
You should see Telegram messages in chat when you add/edit pages.
5. Rate-Limiting & Error Handling
- Notion API limit: 3 req/sec. Worker only calls once per run → safe.
- Telegram limit: 20 msgs/minute. Use
Promise.all
plussetTimeout
if huge bursts. - Wrap fetch in try/catch and
ctx.waitUntil
to ensure completion after response.
6. Security & Maintenance
✅ Secrets stored via wrangler secret
.
✅ KV namespace is free up to 1 GiB.
✅ Rotate Notion integration token yearly.
✅ Add Sentry’s @sentry/cloudflare-worker
for error monitoring.
7. Extending Further
- Support rich embeds (
photo
,document
). - Filter by
Status
property (e.g., only Published). - Include diffs in updates via
before
/after
comparison. - Use Web Subscriptions once Notion releases push webhooks.
8. Conclusion
With <100 lines of code you’ve built a serverless bridge from Notion to Telegram that runs for free and needs zero DevOps. Happy automating! 🤖