Skip to content

Google Calendar

Overview#

The Google Calendar integration connects Cognest to the Google Calendar API using OAuth 2.0. It supports creating, reading, updating, and deleting calendar events, querying free/busy availability, and receiving push notifications when events change. This integration is ideal for building scheduling assistants, meeting coordinators, and automated calendar management workflows.

Prerequisites#

  1. A Google Cloud project with the Google Calendar API enabled
  2. OAuth 2.0 credentials (Client ID and Client Secret) from the Google Cloud Console
  3. Authorized redirect URI pointing to your Cognest instance's OAuth callback endpoint
  4. A verified domain for push notification delivery (required by Google for watch channels)

OAuth Scopes

Cognest requests the calendar and calendar.events scopes by default. For read-only access, you can restrict to calendar.readonly and calendar.events.readonly. Users authorize these scopes during the OAuth consent flow.

Configuration#

Add the Google Calendar integration to your cognest.config.yaml:

cognest.config.yaml
integrations:
  google-calendar:
    enabled: true
    credentials:
      client_id: ${GOOGLE_CALENDAR_CLIENT_ID}
      client_secret: ${GOOGLE_CALENDAR_CLIENT_SECRET}
    settings:
      oauth_callback_path: "/auth/google-calendar/callback"
      token_storage: "encrypted-file"
      scopes:
        - calendar
        - calendar.events
      default_calendar: "primary"
      timezone: "America/New_York"
      watch:
        enabled: true
        notification_path: "/webhooks/google-calendar"
        renew_interval: 86400
      reminder_defaults:
        - method: "popup"
          minutes: 10

Store your credentials in environment variables:

.env
# .env
GOOGLE_CALENDAR_CLIENT_ID=829471035284-xyz789calendar.apps.googleusercontent.com
GOOGLE_CALENDAR_CLIENT_SECRET=GOCSPX-CalEnDaRsEcReTvAlUe

SDK Usage#

Register the integration programmatically for full control:

index.ts
import { Cognest } from '@cognest/sdk'
import { GoogleCalendar } from '@cognest/integrations/google-calendar'

const cognest = new Cognest({ configPath: false })

cognest.use(GoogleCalendar, {
  clientId: process.env.GOOGLE_CALENDAR_CLIENT_ID!,
  clientSecret: process.env.GOOGLE_CALENDAR_CLIENT_SECRET!,
  settings: {
    oauthCallbackPath: '/auth/google-calendar/callback',
    tokenStorage: 'encrypted-file',
    scopes: ['calendar', 'calendar.events'],
    defaultCalendar: 'primary',
    timezone: 'America/New_York',
    watch: {
      enabled: true,
      notificationPath: '/webhooks/google-calendar',
      renewInterval: 86_400,
    },
    reminderDefaults: [
      { method: 'popup', minutes: 10 },
    ],
  },
})

await cognest.start()

OAuth Flow#

Like the Gmail integration, Google Calendar requires OAuth 2.0 authorization. The SDK handles token storage and refresh:

auth.ts
const gcal = cognest.integration<GoogleCalendar>('google-calendar')

// Generate the authorization URL
const authUrl = gcal.getAuthUrl({
  redirectUri: 'https://your-app.com/auth/google-calendar/callback',
  accessType: 'offline',
  prompt: 'consent',
})

// After the user authorizes, exchange the code for tokens
await gcal.handleCallback(authorizationCode)

Available Methods#

Access Google Calendar methods through cognest.integration('google-calendar'):

MethodParametersDescription
`createEvent(event, options?)``event`: `{ summary, start, end, description?, attendees?, location? }`Create a new calendar event with optional attendees and reminders
`updateEvent(eventId, updates, options?)``eventId`: event to update, `updates`: partial event fieldsUpdate an existing event's time, title, description, or attendees
`deleteEvent(eventId, options?)``eventId`: event to remove, `options`: `{ sendUpdates? }`Delete an event and optionally notify attendees
`listEvents(options?)``{ timeMin?, timeMax?, maxResults?, calendarId?, q? }`List events within a time range with optional text search
`checkAvailability(params)``{ timeMin, timeMax, items: [{ id }] }`Query free/busy information for one or more calendars
`watchChanges(options?)``{ calendarId?, ttl? }`Register a push notification channel for real-time event changes

Events#

Events are delivered via Google Calendar push notifications. Enable the watch setting in your configuration to receive real-time updates:

EventPayloadDescription
`calendar:event.created``{ eventId, calendarId, summary, start, end, creator, attendees }`A new event was created on the watched calendar
`calendar:event.updated``{ eventId, calendarId, summary, start, end, changes, updatedFields }`An existing event was modified (time, title, attendees, etc.)
`calendar:event.deleted``{ eventId, calendarId, summary }`An event was removed from the calendar
`calendar:reminder``{ eventId, calendarId, summary, start, minutesBefore }`A configured reminder triggered before an upcoming event

Example#

A complete scheduling assistant that creates events from natural language, checks availability, and sends reminders:

scheduler.ts
import { Cognest } from '@cognest/sdk'
import { GoogleCalendar } from '@cognest/integrations/google-calendar'
import { Telegram } from '@cognest/integrations/telegram'

const cognest = new Cognest()
const gcal = cognest.integration<GoogleCalendar>('google-calendar')
const tg = cognest.integration<Telegram>('telegram')

// Parse natural language scheduling requests
cognest.on('telegram:message', async (ctx) => {
  const { chatId, text } = ctx.payload

  // Use AI to extract scheduling intent
  const parsed = await cognest.engine.structured({
    message: text,
    schema: {
      type: 'object',
      properties: {
        intent: { enum: ['create_event', 'check_schedule', 'cancel_event', 'other'] },
        summary: { type: 'string' },
        date: { type: 'string', format: 'date' },
        startTime: { type: 'string' },
        endTime: { type: 'string' },
        attendees: { type: 'array', items: { type: 'string' } },
      },
    },
  })

  if (parsed.intent === 'create_event') {
    // Check availability first
    const startDateTime = new Date(`${parsed.date}T${parsed.startTime}`)
    const endDateTime = new Date(`${parsed.date}T${parsed.endTime}`)

    const availability = await gcal.checkAvailability({
      timeMin: startDateTime.toISOString(),
      timeMax: endDateTime.toISOString(),
      items: [{ id: 'primary' }],
    })

    const isBusy = availability.calendars.primary.busy.length > 0

    if (isBusy) {
      await tg.send(chatId, [
        'That time slot is not available. You have a conflict:',
        '',
        ...availability.calendars.primary.busy.map(
          (b) => `  - Busy from ${b.start} to ${b.end}`
        ),
        '',
        'Would you like me to find the next available slot?',
      ].join('\n'))
      return
    }

    // Create the event
    const event = await gcal.createEvent({
      summary: parsed.summary,
      start: {
        dateTime: startDateTime.toISOString(),
        timeZone: 'America/New_York',
      },
      end: {
        dateTime: endDateTime.toISOString(),
        timeZone: 'America/New_York',
      },
      attendees: parsed.attendees?.map((email) => ({ email })),
      reminders: {
        useDefault: false,
        overrides: [
          { method: 'popup', minutes: 10 },
        ],
      },
    })

    await tg.send(chatId, [
      `Event created: ${event.summary}`,
      `Date: ${parsed.date}`,
      `Time: ${parsed.startTime} - ${parsed.endTime}`,
      `Link: ${event.htmlLink}`,
    ].join('\n'))
  }

  if (parsed.intent === 'check_schedule') {
    const dayStart = new Date(`${parsed.date}T00:00:00`)
    const dayEnd = new Date(`${parsed.date}T23:59:59`)

    const events = await gcal.listEvents({
      timeMin: dayStart.toISOString(),
      timeMax: dayEnd.toISOString(),
      singleEvents: true,
      orderBy: 'startTime',
    })

    if (events.items.length === 0) {
      await tg.send(chatId, `No events scheduled for ${parsed.date}.`)
      return
    }

    const schedule = events.items.map((e) => {
      const start = new Date(e.start.dateTime || e.start.date)
      const time = start.toLocaleTimeString('en-US', {
        hour: '2-digit',
        minute: '2-digit',
      })
      return `  ${time} - ${e.summary}`
    })

    await tg.send(chatId, [
      `Schedule for ${parsed.date}:`,
      '',
      ...schedule,
    ].join('\n'))
  }
})

// Notify via Telegram when a new event is created externally
cognest.on('calendar:event.created', async (ctx) => {
  const { summary, start, end, attendees } = ctx.payload

  const startDate = new Date(start.dateTime || start.date)
  const formattedTime = startDate.toLocaleString('en-US', {
    weekday: 'long',
    month: 'short',
    day: 'numeric',
    hour: '2-digit',
    minute: '2-digit',
  })

  // Send notification to the configured Telegram chat
  const notifyChatId = process.env.TELEGRAM_NOTIFY_CHAT_ID!
  await tg.send(notifyChatId, [
    '📅 New event added to your calendar:',
    '',
    `<b>${summary}</b>`,
    formattedTime,
    attendees?.length
      ? `Attendees: ${attendees.map((a) => a.email).join(', ')}`
      : '',
  ].join('\n'))
})

// Send reminder notifications
cognest.on('calendar:reminder', async (ctx) => {
  const { summary, start, minutesBefore } = ctx.payload

  const notifyChatId = process.env.TELEGRAM_NOTIFY_CHAT_ID!
  await tg.send(notifyChatId, [
    `Reminder: <b>${summary}</b> starts in ${minutesBefore} minutes.`,
  ].join('\n'))
})

await cognest.start()
console.log('Scheduling assistant is running')

Troubleshooting#

Common Issues

Push notifications not arriving: Google requires your notification endpoint to be on a verified domain with a valid SSL certificate. Verify ownership in the Google Search Console and ensure your endpoint responds with a 200 status to channel setup requests. Watch channel expires: Google Calendar push notification channels expire after the configured TTL (default 24 hours). Cognest automatically renews channels, but if your server was down during renewal, you may miss events. The SDK re-establishes watch channels on startup. insufficient_scope error: The user's access token does not include the required Calendar scopes. Trigger a new OAuth flow with the correct scopes and set prompt to consent to ensure the user sees the updated permission request. Event times off by hours: Always specify the timeZone field when creating or updating events. If omitted, the API defaults to UTC, which can cause events to appear at unexpected times for users in other time zones.