Skip to content

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:

  1. Cache-First: All reads happen from local database, eliminating API latency
  2. Offline-Friendly: Create/edit/delete events without internet connection
  3. 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:

yaml
calendar:
  enabled: true
  verified_client: "your-client-id@apps.googleusercontent.com"

The calendar worker uses OAuth2 credentials from the IMAP configuration:

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

python
self.window_days_past = 30    # Days in the past
self.window_days_future = 90  # Days in the future

Features

Instant Calendar Queries

All calendar reads happen from the local cache:

python
# 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:

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

Status 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 ⏱ Pending badge)
  • conflict: Offline edit conflicted with server changes (shows ⚠ Conflict badge)

Calendar Selection

Users can choose which calendars to display via the web UI:

  1. Navigate to SettingsCalendar section
  2. Check/uncheck calendars to show/hide
  3. 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:

  1. Flush Outbox: Process pending create/patch/delete operations
  2. Incremental Sync: Fetch changes since last sync using sync tokens
  3. Update Cache: Store new/updated events in local database
  4. 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:

bash
docker-compose logs -f workspace-secretary | grep calendar-worker

Look for:

  • === Starting sync cycle ===
  • Processing N pending outbox operations
  • Incremental sync for calendar_id: N changes
  • Full 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 conflict for user review
  • Crash recovery: Supervisor restarts worker automatically

Database Schema

calendar_events_cache

Stores cached events for instant queries:

ColumnTypeDescription
calendar_idTEXTGoogle Calendar ID
event_idTEXTEvent ID (or local:<uuid> for pending)
event_jsonJSONFull event data from Google Calendar API
start_ts_utcINTEGERStart timestamp (epoch) for fast range queries
start_dateTEXTStart date (YYYY-MM-DD) for day-based queries
local_statusTEXTSync status: synced, pending, conflict
cached_atTIMESTAMPWhen 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:

ColumnTypeDescription
op_idSERIALUnique operation ID
calendar_idTEXTTarget calendar
event_idTEXTEvent ID (null for creates)
op_typeTEXTOperation: create, patch, delete
payloadJSONEvent data for create/patch
statusTEXTpending, applied, failed
created_atTIMESTAMPWhen operation was queued
errorTEXTError message if failed

calendar_sync_state

Tracks sync progress per calendar:

ColumnTypeDescription
calendar_idTEXTGoogle Calendar ID
window_startTEXTStart of cached time window (RFC3339)
window_endTEXTEnd of cached time window (RFC3339)
sync_tokenTEXTGoogle Calendar sync token for incremental sync
statusTEXTok, error
last_full_sync_atTIMESTAMPLast full sync timestamp
last_incremental_sync_atTIMESTAMPLast incremental sync timestamp
last_errorTEXTMost recent error message

Performance

Benchmarks

OperationBefore (Direct API)After (Cached)Improvement
List events (1 week)~500ms~50ms10x faster
List events (1 month)~1200ms~80ms15x faster
Calendar page load~1500ms~150ms10x 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:

  1. Worker detects conflict during sync
  2. Event marked with local_status="conflict"
  3. User sees ⚠ Conflict badge in UI
  4. Server version is preserved in cache
  5. User can manually resolve by re-editing event

Future enhancement: Side-by-side conflict resolution UI.

Troubleshooting

Calendar Not Syncing

  1. Check worker is running:

    bash
    docker-compose ps | grep calendar-worker
  2. Check worker logs for errors:

    bash
    docker-compose logs calendar-worker
  3. Verify OAuth2 token is valid:

    bash
    ls -la config/gmail-token.json

Events Not Appearing

  1. Check calendar is selected in settings:

    • Open Settings → Calendar
    • Ensure calendar checkbox is checked
  2. Check sync state in database:

    sql
    SELECT * FROM calendar_sync_state WHERE calendar_id = 'your-calendar-id';
  3. Force full sync by deleting sync token:

    sql
    UPDATE calendar_sync_state SET sync_token = NULL WHERE calendar_id = 'your-calendar-id';

Pending Events Not Syncing

  1. Check outbox for failed operations:

    sql
    SELECT * FROM calendar_outbox WHERE status = 'failed';
  2. Review error messages in outbox:

    sql
    SELECT op_id, op_type, error FROM calendar_outbox WHERE status = 'failed';
  3. Manually retry by resetting status:

    sql
    UPDATE 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

python
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

python
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

python
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

Released under the MIT License.