A Neovim plugin to synchronize Google Calendar events with org-mode notes, featuring full bidirectional sync, agenda integration, and optional org-roam backlinks.
I've been using orgmode in Neovim, and I wanted to be able to sync my agenda files. Google Calendar is my primary calendar service, so I built this plugin to bridge the gap. It allows me to keep my org notes and tasks in sync with my Google Calendar events, ensuring I never miss an important meeting or deadline.
- GCal → org notes with
#+title,SCHEDULED, and optionalROAM_REFS - org tasks → GCal
- Appears in
org-agenda - Backlinks to any note mentioning the event (requires org-roam)
- Duplicate-safe with event ID tracking
- Unit tested
- Advanced sync: Updates and deletions are synchronized bidirectionally
- Direct Google Calendar API integration (no external dependencies)
- Auto-sync on save: Automatically syncs when saving org files with scheduled events
- Multiple calendars: Sync with multiple Google Calendars
- Recurring events: Full support for recurring event patterns
- Conflict resolution: Smart conflict detection with interactive resolution UI
- Sync dashboard: Visual status dashboard with statistics
- Per-directory calendars: Map different org directories to different calendars
- Webhook support: Real-time sync via Google Calendar push notifications
- Optional org-roam integration: Works with or without org-roam
- Neovim 0.9+
- plenary.nvim
- nvim-orgmode/orgmode
- org-roam.nvim (optional, for backlinks)
- Google Calendar API credentials (see setup below)
- Go to Google Cloud Console
- Create a new project or select an existing one
- Enable the Google Calendar API:
- Navigate to "APIs & Services" > "Library"
- Search for "Google Calendar API"
- Click "Enable"
- Go to "APIs & Services" > "Credentials"
- Click "Create Credentials" > "OAuth client ID"
- If prompted, configure the OAuth consent screen:
- Choose "External" for user type
- Fill in app name and your email
- Add scope:
https://www.googleapis.com/auth/calendar
- For application type, select "Desktop app"
- Give it a name (e.g., "org-gcal-sync")
- Click "Create"
- Download the credentials JSON or copy the Client ID and Client Secret
Add these to your shell configuration (.bashrc, .zshrc, etc.):
export GCAL_ORG_SYNC_CLIENT_ID="your-client-id-here.apps.googleusercontent.com"
export GCAL_ORG_SYNC_CLIENT_SECRET="your-client-secret-here"Reload your shell or restart Neovim to apply the changes.
{
"eprislac/org-gcal-sync",
dependencies = {
"nvim-lua/plenary.nvim",
"nvim-orgmode/orgmode",
"jmbuhr/org-roam.nvim" -- optional
},
config = function()
require("org-gcal-sync").setup({
org_dirs = { "~/org/personal", "~/org/work" },
enable_backlinks = true, -- requires org-roam
auto_sync_on_save = true,
-- Advanced features (optional)
calendars = { "primary" }, -- Add more calendars: { "primary", "[email protected]" }
sync_recurring_events = true,
conflict_resolution = "ask", -- "ask", "local", "remote", or "newest"
show_sync_status = true,
-- per_directory_calendars = { ["~/org/work"] = "[email protected]" },
-- webhook_port = 8080,
})
end,
}{
"eprislac/org-gcal-sync",
dependencies = {
"nvim-lua/plenary.nvim",
"nvim-orgmode/orgmode"
},
config = function()
require("org-gcal-sync").setup({
org_dirs = { "~/org" },
enable_backlinks = false, -- org-roam not available
auto_sync_on_save = true,
})
end,
}See ADVANCED_FEATURES.md for detailed configuration options.
After installation and setting up environment variables:
- Run
:OrgGcalAuthin Neovim - Your browser will open to Google's OAuth consent page
- Authorize the application
- Copy the authorization code from the browser
- Paste it into the Neovim prompt
- Authentication token will be saved to
~/.local/share/nvim/org-gcal-sync/token.json
The token will be automatically refreshed when needed.
:SyncOrgGcal- Full bidirectional sync (export + import):ImportGcal- Import events from Google Calendar to org files:ExportOrg- Export org tasks to Google Calendar:OrgGcalAuth- Authenticate with Google Calendar:OrgGcalDashboard- Show sync status dashboard:OrgGcalListCalendars- List available calendars:OrgGcalWebhookStart- Start webhook server for real-time sync:OrgGcalWebhookStop- Stop webhook server
From Google Calendar to Org:
- Event title →
#+titleand headline - Start time →
SCHEDULED - Location →
:LOCATION:property - Description → body text
- Event ID →
:GCAL_ID:property (for tracking updates/deletions) - Last modified time →
:GCAL_UPDATED:property
From Org to Google Calendar:
- TODO/NEXT items with
SCHEDULEDorDEADLINEtimestamps - Scheduled TODOs → Google Calendar events (with times)
- Unscheduled TODOs → Google Tasks
:LOCATION:property → Event location- Body text → Event description
- Updates existing events if they have a
:GCAL_ID:property
The plugin supports standard org-mode timestamp formats:
Timed events (with specific time):
* TODO Take Blood Pressure
SCHEDULED: <2025-10-30 Thu 10:00 .+1d>
→ Creates calendar event at 10:00 AM - 10:30 AM (30-minute default duration)
Timed events with time range:
* TODO Team Meeting
SCHEDULED: <2025-10-30 Thu 14:00>--<2025-10-30 Thu 15:30>
→ Creates calendar event at 2:00 PM - 3:30 PM
All-day events (date only):
* TODO Review Documents
SCHEDULED: <2025-10-30 Sat>
→ Creates all-day event on Oct 30
Recurring events:
* TODO Daily Standup
SCHEDULED: <2025-10-30 Thu 09:00 .+1d>
→ Repeats daily (.+1d = daily, .+1w = weekly, .+1m = monthly)
Priority tags are supported:
* TODO [#A] High Priority Task
SCHEDULED: <2025-10-30 Thu 10:00>
Events are tracked using:
- Google Calendar Event ID (stored in
:GCAL_ID:) - Fallback to title + timestamp matching
This ensures:
- Updates to events are synchronized correctly
- Events deleted from Google Calendar are removed from org files
- No duplicate events are created
Run :checkhealth org-gcal-sync to verify:
- Environment variables are set
- Google Calendar API is reachable
- plenary.nvim is installed
Example: Imported calendar event
:PROPERTIES:
:ID: 8b2c3d3e-9800-4186-80e5-d07ce7bc5327
:END:
#+title: Team Standup
#+filetags: :gcal:
* Team Standup
SCHEDULED: <2025-11-01 Fri 14:00>--<2025-11-01 Fri 14:30>
:PROPERTIES:
:GCAL_ID: abc123xyz
:LOCATION: Conference Room A
:GCAL_UPDATED: 2025-10-29T10:00:00Z
:END:
Discussion points for the daily standup meeting.
Example: TODO that syncs to calendar
:PROPERTIES:
:ID: 2F80BBB3-F46C-42E5-9831-C25DDCD060C0
:CATEGORY: Work
:END:
#+TITLE Daily Review
#+FILETAGS: #work #routine
* TODO [#A] Daily Review
SCHEDULED: <2025-10-30 Thu 17:00 .+1d>--<2025-10-30 Thu 17:30>
:PROPERTIES:
:GCAL_ID: xyz789abc
:LAST_REPEAT: [2025-10-29 Wed 17:25]
:END:
:LOGBOOK:
- State "DONE" from "TODO" [2025-10-29 Wed 17:25]
:END:
Review tasks and plan tomorrow.
Example: Unscheduled TODO that syncs to Google Tasks
* TODO [#B] Research new library
:PROPERTIES:
:GCAL_ID: task_def456
:END:
Look into the new data processing library for the project.
Run tests with:
nvim --headless -c "PlenaryBustedDirectory tests/plenary { minimal_init = 'tests/minimal_init.lua' }"Tests mock the Google Calendar API to avoid requiring actual credentials during testing.
- Run
:OrgGcalAuthto authenticate - Check that environment variables are set correctly
- The plugin should auto-refresh, but if it fails, run
:OrgGcalAuthagain
- Check
:checkhealth org-gcal-sync - Verify your org files have
SCHEDULEDorDEADLINEtimestamps - Make sure TODOs are marked with
TODOorNEXTkeywords - Ensure timestamps follow org-mode format:
- Timed:
<2025-10-30 Thu 10:00>or<2025-10-30 10:00> - All-day:
<2025-10-30>or<2025-10-30 Thu> - Range:
<2025-10-30 Thu 10:00>--<2025-10-30 Thu 11:00>
- Timed:
- Make sure your timestamp includes a time:
<2025-10-30 Thu 10:00> - Date-only timestamps
<2025-10-30>create all-day events by design - Time ranges use
--separator:<START>--<END>
- The plugin automatically detects and removes duplicates
- Check for events with the same
:GCAL_ID:property - Run
:SyncOrgGcalto clean up duplicates automatically
- Unscheduled TODOs sync to Google Tasks
- Make sure the TODO doesn't have a
SCHEDULEDorDEADLINEtimestamp - Scheduled TODOs go to Google Calendar, not Tasks
- Google Calendar API has quotas (usually 1,000,000 queries/day)
- The plugin batches requests efficiently, but be mindful with very large calendars
MIT