• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
project logo

liamh

google-calendar-linear-sync

Bidirectional sync for Linear issues and Google Calendar events
Public
Like
1
google-calendar-linear-sync
Home
Code
12
.claude
1
linear-gcal-bidirectional-sync
scripts
5
src
18
.env.example
.gitignore
.vtignore
CLAUDE.md
README.md
deno.json
standalone-val-town-sync.ts
C
val-town-sync.cron.ts
Branches
1
Pull requests
Remixes
History
Environment variables
5
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
README.md
Code
/
README.md
Search
7/19/2025
Viewing readonly version of main branch: v123
View latest version
README.md

Google Calendar ↔ Linear Sync Worker

A stateless, deterministic sync worker implementing the Declarative Reconciliation Loop (DRL) pattern to bidirectionally sync Google Calendar events and Linear issues.

Architecture Overview

This refactored implementation follows a strict Observe β†’ Project β†’ Diff β†’ Actuate pattern:

  1. Observe: Fetch current snapshots from Linear and Google Calendar APIs
  2. Project: Create a canonical truth using deterministic, side-effect-free functions
  3. Diff: Compare canonical state with external reality to compute minimal operations
  4. Actuate: Execute idempotent API calls until external systems match canonical truth

Key Design Principles

  • Stateless: No database required; all state lives in remote systems via UID linkages
  • Deterministic: Pure functions ensure identical inputs always produce identical outputs
  • Idempotent: Operations can be safely retried without side effects
  • Minimal: Only necessary changes are made to external systems

Core Data Models

Google Calendar Event (GCalEvent)

interface GCalEvent { id: string; // Primary key (never edited) summary: string; // Prefix + human title description?: string; // Freeform notes start: { dateTime: string }; // ISO 8601 UTC end: { dateTime: string }; // ISO 8601 UTC extendedProperties?: { private?: { uid?: string; // Canonical UID linking to Linear linearIssueId?: string; // Back-pointer to Linear issue }; }; status: "confirmed" | "cancelled"; }

Linear Issue (LinearIssue)

interface LinearIssue { id: string; // Primary key title: string; // Prefix + human title description?: string; // Long notes + calendar metadata state: "Triage" | "Scheduled" | "Done" | "Canceled" | "Failed"; targetDate?: string; // ISO 8601 datetime // Note: Linear API doesn't support custom fields - use description metadata instead }

Canonical Item (CanonicalItem)

The unified representation that bridges both systems:

interface CanonicalItem { uid: string; // Stable identifier across systems title: string; // Clean title (no prefix) description?: string; startTime?: string; // ISO 8601 endTime?: string; // ISO 8601 linearId?: string; gcalId?: string; linearState?: LinearIssue["state"]; phase: Phase; // Current lifecycle phase lastModified: string; // For conflict resolution }

Phase Transitions

The system recognizes 5 phases with specific transition rules:

PhaseDescriptionTriggers
eventOnlyGCal event without Linear matchNew calendar event created
linearOnlyLinear issue without GCal matchIssue marked as "Scheduled"
activeLinked and syncingBoth systems have records
completedLinear marked done/canceled/failedState change in Linear
overduePast deadline but still active>24h after event end

Transition Actions

From β†’ ToTriggerActionResult Prefix
eventOnly β†’ activeGCal event existsCreate Linear issue (Triage)πŸ“₯
linearOnly β†’ activeLinear state = "Scheduled"Create GCal eventπŸ“…
active β†’ completedLinear state = Done/Canceled/FailedUpdate GCal titleβœ…/🚫/❌
active β†’ overdue>24h past event endMark original as worked (⏳) + create new event⏳ + πŸ“…

Prefix Meanings

  • πŸ“₯ Triage: New items from Google Calendar awaiting review
  • πŸ“… Scheduled: Items scheduled for specific times
  • βœ… Done: Completed items
  • 🚫 Canceled: Canceled items
  • ❌ Failed: Failed items
  • ⏳ Worked: Original events that went overdue (shows work was done)

| active β†’ active | Title/time/description change | Sync metadata | (unchanged) |

Bidirectional Linking

The system uses two linking mechanisms:

  1. GCal β†’ Linear: extendedProperties.private.linearIssueId in calendar events
  2. Linear β†’ GCal: Description metadata <!-- calendar-sync --> GoogleCalEventId:xyz in Linear issues

This approach works because:

  • Google Calendar API supports extendedProperties for metadata storage
  • Linear API doesn't support custom fields, so we embed metadata in descriptions
  • Both systems preserve their metadata across updates

Conflict Resolution

  • Title: Linear wins (source of truth for task names)
  • Start/End Times: Calendar wins (source of truth for scheduling)
  • Description: Newest updatedAt wins (simplified to Linear in current implementation)

File Structure

src/
β”œβ”€β”€ types.ts          # Core data models and interfaces
β”œβ”€β”€ projector.ts      # Observe β†’ Project (create canonical truth)
β”œβ”€β”€ diff.ts           # Project β†’ Diff (compute operations)
β”œβ”€β”€ actuator.ts       # Diff β†’ Actuate (execute operations)
β”œβ”€β”€ worker.ts         # Main sync orchestrator
β”œβ”€β”€ main.ts           # Entry point with configuration
└── *.test.ts         # Comprehensive test suite

Usage

Running Tests

deno test --allow-env --allow-net --allow-import src/*.test.ts

Running Sync

# Set environment variables export LINEAR_API_KEY="your-linear-api-key" export LINEAR_TEAM_ID="your-team-id" export GCAL_API_KEY="your-google-api-key" export GCAL_CALENDAR_ID="your-calendar-id" # Run sync deno run --allow-env --allow-net src/main.ts

Configuration

The worker is configured via environment variables:

  • LINEAR_API_KEY: Linear API token
  • LINEAR_TEAM_ID: Linear team identifier
  • GCAL_API_KEY: Google Calendar API key
  • GCAL_CALENDAR_ID: Primary calendar ID
  • GCAL_HISTORY_CALENDAR_ID: (Optional) Calendar for overdue items
  • TIMEZONE: Timezone for operations (default: UTC)
  • LOOKBACK_DAYS: Days to look back (default: 30)
  • LOOKAHEAD_DAYS: Days to look ahead (default: 90)

Performance Characteristics

  • Latency: < 5 seconds for 10,000 items (per spec requirement)
  • Memory: Stateless operation, minimal memory footprint
  • Network: Batched API calls, efficient pagination
  • Reliability: Idempotent operations survive failures

Testing Strategy

The test suite covers:

  • Unit tests: Individual functions (types, projector, diff)
  • Integration tests: Full sync workflows with mock APIs
  • Determinism: Identical inputs produce identical outputs
  • Idempotence: Operations can be safely retried
  • Performance: Validates < 5s execution time

Extending the System

The DRL pattern makes it easy to:

  1. Add new external systems (e.g., Notion) by implementing new projectors/actuators
  2. Add new phase transitions by updating the diff engine
  3. Add new conflict resolution rules in the projector
  4. Add webhook triggers by replacing the cron scheduler

Migration from Legacy

The original implementation has been moved to backup/ and key concepts have been preserved:

  • Bidirectional sync logic
  • State machine approach (now phase-based)
  • Metadata embedding for linkage
  • Prefix-based visual indicators

The new implementation is more robust, testable, and maintainable while preserving all original functionality.

FeaturesVersion controlCode intelligenceCLIMCP
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
PricingNewsletterBlogAboutCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
Β© 2026 Val Town, Inc.