Calendar Integration
Workspace Secretary provides a powerful offline-first calendar integration with Google Calendar, featuring intelligent caching, background synchronization, and seamless offline operation.
Overview
The calendar system is built around three core principles:
- Cache-First: All reads happen from local database, eliminating API latency
- Offline-Friendly: Create/edit/delete events without internet connection
- Transparent Sync: Background worker handles synchronization automatically
Architecture
┌─────────────────────────────────────────────────────────────┐
│ User/LLM │
└────────────────┬────────────────────────────────────────────┘
│ Read: ~50ms
▼
┌─────────────────────────────────────────────────────────────┐
│ Engine API │
│ • GET /api/calendar/events → calendar_events_cache │
│ • POST /api/calendar/event → calendar_outbox (pending) │
└────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Database (PostgreSQL/SQLite) │
│ • calendar_events_cache: Local event storage │
│ • calendar_outbox: Pending operations queue │
│ • calendar_sync_state: Sync tokens & health │
└─────────────────────────────────────────────────────────────┘
▲
│ Sync every 60s
│
┌────────────────┴────────────────────────────────────────────┐
│ Calendar Worker (Background Process) │
│ • Flush outbox → Google Calendar API │
│ • Incremental sync using sync tokens │
│ • Full sync every 24 hours │
└─────────────────────────────────────────────────────────────┘Configuration
Enable Calendar Sync
In config/config.yaml:
calendar:
enabled: true
verified_client: "your-client-id@apps.googleusercontent.com"The calendar worker uses OAuth2 credentials from the IMAP configuration:
imap:
oauth2:
enabled: true
credentials_file: "config/gmail-credentials.json"
token_file: "config/gmail-token.json"Worker Configuration
The calendar worker is automatically started by supervisord. Default settings:
- Sync Interval: 60 seconds (incremental sync)
- Full Sync: Every 24 hours
- Time Window: -30 days to +90 days
- Priority: 40 (starts after engine/web)
To adjust the time window, modify workspace_secretary/engine/calendar_worker.py:
self.window_days_past = 30 # Days in the past
self.window_days_future = 90 # Days in the futureFeatures
Instant Calendar Queries
All calendar reads happen from the local cache:
# MCP Tool usage (for LLMs)
events = await list_calendar_events(
time_min="2026-01-12T00:00:00Z",
time_max="2026-01-19T23:59:59Z"
)
# Returns instantly from cache (~50ms)Offline Event Creation
Create events without internet connection:
event = await create_calendar_event(
summary="Team Sync",
start="2026-01-15T10:00:00Z",
end="2026-01-15T11:00:00Z",
location="Conference Room A"
)
# Returns immediately with local:<uuid> ID
# Syncs to Google Calendar when worker runsStatus Tracking
Events include a _local_status field indicating sync state:
synced: Event is synchronized with Google Calendar (default, no badge shown)pending: Event awaiting background sync (shows⏱ Pendingbadge)conflict: Offline edit conflicted with server changes (shows⚠ Conflictbadge)
Calendar Selection
Users can choose which calendars to display via the web UI:
- Navigate to Settings → Calendar section
- Check/uncheck calendars to show/hide
- Changes take effect immediately
Selected calendars are stored in user preferences and respected by both the web UI and MCP tools.
Web UI
Calendar Views
The calendar interface provides multiple views:
- Day View: Hourly breakdown with timed events
- Week View: 7-day grid with event blocks
- Month View: Monthly calendar with event pills
- Agenda View: List of upcoming events with details
All views show status badges for pending/conflict events.
Event Management
- Create: Click time slot → Fill form → Saves to outbox → Syncs in background
- Edit: Click event → Modify details → Queues update → Syncs in background
- Delete: Click event → Delete → Soft-delete → Syncs in background
Status Indicators
Events display visual indicators based on sync state:
- No Badge: Event is synced with Google Calendar
- ⏱ Pending: Event/update awaiting sync to Google
- ⚠ Conflict: Offline edit conflicted, needs resolution
Background Sync Worker
How It Works
The calendar worker runs continuously as a supervised process:
- Flush Outbox: Process pending create/patch/delete operations
- Incremental Sync: Fetch changes since last sync using sync tokens
- Update Cache: Store new/updated events in local database
- Repeat: Wait 60 seconds, run again
Every 24 hours, a full sync is performed to refresh the entire time window.
Sync Tokens
The worker uses Google Calendar API's sync tokens for efficient incremental sync:
- Only fetches events that changed since last sync
- Detects new events, updates, and deletions
- Handles sync token invalidation gracefully (automatic full sync)
Monitoring
Check worker logs in Docker:
docker-compose logs -f workspace-secretary | grep calendar-workerLook for:
=== Starting sync cycle ===Processing N pending outbox operationsIncremental sync for calendar_id: N changesFull sync for calendar_id: fetched N events=== Sync cycle completed ===
Error Handling
The worker handles errors gracefully:
- Sync token invalid: Automatically performs full sync
- Network failure: Retries on next cycle (60s later)
- Conflict detection: Marks events as
conflictfor user review - Crash recovery: Supervisor restarts worker automatically
Database Schema
calendar_events_cache
Stores cached events for instant queries:
| Column | Type | Description |
|---|---|---|
calendar_id | TEXT | Google Calendar ID |
event_id | TEXT | Event ID (or local:<uuid> for pending) |
event_json | JSON | Full event data from Google Calendar API |
start_ts_utc | INTEGER | Start timestamp (epoch) for fast range queries |
start_date | TEXT | Start date (YYYY-MM-DD) for day-based queries |
local_status | TEXT | Sync status: synced, pending, conflict |
cached_at | TIMESTAMP | When event was cached |
Indexes:
(calendar_id, start_ts_utc)for time-based queries(calendar_id, start_date)for day-based queries
calendar_outbox
Queues offline operations for background sync:
| Column | Type | Description |
|---|---|---|
op_id | SERIAL | Unique operation ID |
calendar_id | TEXT | Target calendar |
event_id | TEXT | Event ID (null for creates) |
op_type | TEXT | Operation: create, patch, delete |
payload | JSON | Event data for create/patch |
status | TEXT | pending, applied, failed |
created_at | TIMESTAMP | When operation was queued |
error | TEXT | Error message if failed |
calendar_sync_state
Tracks sync progress per calendar:
| Column | Type | Description |
|---|---|---|
calendar_id | TEXT | Google Calendar ID |
window_start | TEXT | Start of cached time window (RFC3339) |
window_end | TEXT | End of cached time window (RFC3339) |
sync_token | TEXT | Google Calendar sync token for incremental sync |
status | TEXT | ok, error |
last_full_sync_at | TIMESTAMP | Last full sync timestamp |
last_incremental_sync_at | TIMESTAMP | Last incremental sync timestamp |
last_error | TEXT | Most recent error message |
Performance
Benchmarks
| Operation | Before (Direct API) | After (Cached) | Improvement |
|---|---|---|---|
| List events (1 week) | ~500ms | ~50ms | 10x faster |
| List events (1 month) | ~1200ms | ~80ms | 15x faster |
| Calendar page load | ~1500ms | ~150ms | 10x faster |
| Create event | ~300ms (blocking) | ~5ms (queued) | 60x faster |
Resource Usage
- Database: ~1KB per event (~365KB per year of events)
- Memory: ~50MB for worker process
- CPU: <1% during sync cycles
- Network: Sync traffic only (no per-query API calls)
Conflict Resolution
When an offline edit conflicts with server changes, the system uses a server-wins strategy:
- Worker detects conflict during sync
- Event marked with
local_status="conflict" - User sees
⚠ Conflictbadge in UI - Server version is preserved in cache
- User can manually resolve by re-editing event
Future enhancement: Side-by-side conflict resolution UI.
Troubleshooting
Calendar Not Syncing
Check worker is running:
bashdocker-compose ps | grep calendar-workerCheck worker logs for errors:
bashdocker-compose logs calendar-workerVerify OAuth2 token is valid:
bashls -la config/gmail-token.json
Events Not Appearing
Check calendar is selected in settings:
- Open Settings → Calendar
- Ensure calendar checkbox is checked
Check sync state in database:
sqlSELECT * FROM calendar_sync_state WHERE calendar_id = 'your-calendar-id';Force full sync by deleting sync token:
sqlUPDATE calendar_sync_state SET sync_token = NULL WHERE calendar_id = 'your-calendar-id';
Pending Events Not Syncing
Check outbox for failed operations:
sqlSELECT * FROM calendar_outbox WHERE status = 'failed';Review error messages in outbox:
sqlSELECT op_id, op_type, error FROM calendar_outbox WHERE status = 'failed';Manually retry by resetting status:
sqlUPDATE calendar_outbox SET status = 'pending' WHERE op_id = 123;
MCP Tools for LLMs
LLMs can interact with calendars via FastMCP tools (automatically benefit from caching):
List Events
events = await list_calendar_events(
time_min="2026-01-12T00:00:00Z",
time_max="2026-01-19T23:59:59Z",
calendar_id="primary" # Optional
)Returns list of events with _local_status field indicating sync state.
Create Event
event = await create_calendar_event(
summary="Team Meeting",
start="2026-01-15T10:00:00-08:00",
end="2026-01-15T11:00:00-08:00",
location="Conference Room A",
description="Quarterly planning discussion",
attendees=["alice@example.com", "bob@example.com"]
)Returns immediately with local:<uuid> ID. Syncs in background.
Get Availability
availability = await get_calendar_availability(
time_min="2026-01-15T00:00:00Z",
time_max="2026-01-15T23:59:59Z"
)Uses cached events for instant freebusy calculation.
Next Steps
- Configuration Guide - Full config reference
- Web UI Guide - Web interface documentation
- Docker Deployment - Deployment guide
- Security - Security best practices