One-way sync of Discord scheduled events to a Google Calendar. Runs entirely within Google Apps Script on an hourly trigger — there is no hosted bot process. A Discord bot token is used solely as a read-only API credential.
Google Apps Script (hourly trigger)
│
│ POST (proxied request)
▼
Cloudflare Worker (discord API proxy)
│
│ GET /guilds/{id}/scheduled-events
▼
Discord API
Google Apps Script's UrlFetchApp sends requests from Google's shared IP pool with a user agent that Discord's Cloudflare configuration blocks (error 40333). A lightweight Cloudflare Worker acts as a relay, forwarding requests to the Discord API with a standard bot user agent. The worker is authenticated with a shared secret and only proxies to discord.com/api/v10.
- One-way sync: Discord → Google Calendar only. Never writes to Discord.
- Create, update, delete: New Discord events are created on the calendar, changes are synced, and cancelled events are handled based on your preference.
- Removal behavior: Configurable —
deletethe calendar event,keepit untouched, orstrikeit (prepend ❌ to the title). - Past event protection: If a Discord event disappears after its start time has passed, it's assumed to have ended naturally and is left on the calendar.
- Default duration: Discord events without an end time (voice/stage events) are created as 1-hour events.
- Location mapping: Discord "External" event locations are mapped to the Google Calendar location field.
- Input sanitisation: Fields are truncated before being passed to the Calendar API (name: 500 chars, location: 1000 chars, description: 8000 chars).
- A Discord server you own or have Manage Events permission on
- A Google account
- A Cloudflare account (free tier is sufficient)
- Go to the Discord Developer Portal and create a new application.
- Go to the Installation tab and set the default install link to None (required to make the bot private).
- Go to Bot tab and copy the bot token.
- Under OAuth2 → URL Generator, select the
botscope. Under Bot Permissions, check View Channels and Manage Events. - Open the generated URL in your browser and add the bot to your server.
- Discord Server ID: In Discord, enable Developer Mode (User Settings → Advanced → Developer Mode). Right-click your server name → Copy Server ID.
- Google Calendar ID: Google Calendar → Settings for your target calendar → "Calendar ID" under Integrate calendar. For your primary calendar, this is your Gmail address.
- Go to the Cloudflare dashboard → Workers & Pages → Create.
- Select the "Hello World" script template and name your worker (e.g.
discord-events-sync). - Deploy, then edit the code and paste the contents of
cloudflare-worker/worker.js. Save & Deploy. - Go to the worker's Settings → Variables and Secrets and add a secret:
PROXY_SECRET— a strong random string (e.g. a UUID)
- Note your worker URL (e.g.
https://discord-events-sync.your-subdomain.workers.dev).
-
Go to script.google.com and create a new project.
-
Delete the placeholder code in
Code.gsand paste the contents ofapps-script/Code.gs. -
Go to Project Settings (gear icon) → Script Properties and add:
Property Value DISCORD_BOT_TOKENYour bot token from step 1 DISCORD_GUILD_IDYour server ID from step 2 GOOGLE_CALENDAR_IDYour calendar ID from step 2 PROXY_URLYour Cloudflare Worker URL from step 3 PROXY_SECRETThe same secret you set in the worker DELETE_REMOVED_EVENTSOptional: delete(default),keep, orstrike -
Select
syncDiscordEventsfrom the function dropdown and click Run. Authorize Calendar access when prompted. Check the execution log to confirm events synced. -
Select
installTriggerand run it once to set up the hourly auto-sync.
Each sync cycle:
- Fetches all scheduled events from Discord via the Cloudflare Worker proxy.
- Fetches all managed calendar events (identified by a
[discord-event-id:XXXXXXX]tag in the description). - Creates calendar events for new Discord events.
- Updates calendar events where the Discord event has changed. Unchanged events are skipped.
- Handles calendar events whose Discord event no longer exists, according to the configured removal behavior. Past events are always left untouched.
┌─────────────────────────────────────────────────────────┐
│ Hourly Trigger │
│ syncDiscordEvents() │
└────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────┐
│ Load script props │
│ Validate calendar │
└──────────┬──────────┘
│
┌──────────────┴──────────────┐
▼ ▼
┌───────────────────┐ ┌────────────────────┐
│ Apps Script │ │ Google Calendar │
│ UrlFetchApp │ │ CalendarApp │
│ (POST request) │ │ (1wk back–2yr fwd)│
└─────────┬─────────┘ └──────────┬─────────┘
│ │
▼ │
┌───────────────────┐ │
│ Cloudflare Worker │ │
│ - Auth secret │ │
│ - Validate path │ │
│ - Whitelist method│ │
└─────────┬─────────┘ │
│ │
▼ │
┌───────────────────┐ │
│ Discord API │ │
│ GET /guilds/ │ │
│ {id}/scheduled │ │
│ -events │ │
└─────────┬─────────┘ │
│ │
▼ ▼
┌──────────────────────────────────────────────────────┐
│ Compare Discord ↔ Calendar │
│ (matched by [discord-event-id:XXXX] tag) │
└───┬──────────────────┬───────────────────┬───────────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────────┐ ┌──────────────────┐
│ NEW │ │ EXISTING │ │ GONE FROM DISCORD│
│ │ │ │ │ │
│ Create │ │ Compare all │ │ Past? → Skip │
│ calendar │ │ fields: │ │ │
│ event │ │ - title │ │ Otherwise: │
│ │ │ - start/end │ │ delete: remove │
│ Default │ │ - description│ │ strike: add ❌ │
│ 1hr if │ │ - location │ │ keep: leave it │
│ no end │ │ │ │ │
│ time │ │ Changed? → │ │ Clear sync tag │
│ │ │ Update │ │ (strike/keep) │
│ │ │ Same? → │ │ │
│ │ │ Skip │ │ │
└──────────┘ └──────────────┘ └──────────────────┘
│ │ │
└──────────────────┴───────────────────┘
│
▼
┌─────────────────┐
│ Log summary │
│ Created: N │
│ Updated: N │
│ Unchanged: N │
│ Removed: N │
│ Skipped: N │
│ Errors: N │
└─────────────────┘
- Cloudflare Workers free tier: 100,000 requests/day. An hourly sync uses 24.
- Google Calendar API: Daily quota is generous for personal use. Each create/update/delete is one call.
- Apps Script
UrlFetchApp: 20,000 calls/day for consumer accounts. CalendarApp.getEvents(): Returns up to ~2,500 events per call.
MIT