> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuturuuu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# @tuturuuu/trigger

> Legacy and manual Trigger.dev task helpers

# @tuturuuu/trigger

The `@tuturuuu/trigger` package contains legacy and manual Trigger.dev task
wrappers plus reusable Calendar sync helpers. Production Calendar provider sync
and smart scheduling are owned by `apps/web` cron, with definitions in
`apps/web/cron.config.json`.

Do not add recurring Calendar `schedules.task` jobs here. Calendar provider
sync runs every 15 minutes through `/api/cron/calendar/provider-sync`, which
calls the central workspace sync API with cron auth.

<Note>
  The package uses the **Trigger.dev v4 `task()` API** (`@trigger.dev/sdk`
  `^4.4.5`). The v4 SDK is imported from the `@trigger.dev/sdk/v3` subpath, as
  in `packages/trigger/src`. The legacy v2 API (`client.defineJob`,
  `eventTrigger`, `io.runTask`, `io.sendEvent`) is not used anywhere in this
  repo.
</Note>

<Warning>
  `apps/web` (Next.js, port `7803`) is being migrated to `apps/tanstack-web`
  (TanStack Start) plus `apps/backend` (Rust, port `7820`). See
  [TanStack + Rust migration](/platform/architecture/tanstack-rust-migration).
  Calendar provider sync, the cron wrapper, and the `apps/web` workspace sync
  API remain on `apps/web` for now and have **not** been ported to the Rust
  backend. Only narrow calendar read endpoints (for example the legacy
  `/api/v1/calendar/mock` route) have moved so far; recurring sync stays on
  `apps/web` cron until the migration covers it.
</Warning>

## Installation

```bash theme={null}
# Already included in monorepo workspace
import { googleCalendarSync } from '@tuturuuu/trigger';
```

## Available Jobs

<Note>
  The package's public surface (`packages/trigger/src/index.ts`) currently
  exports `googleCalendarFullSync`, `googleCalendarFullSyncOrchestrator`,
  `performFullSyncForWorkspace`, `unifiedScheduleTask`,
  `unifiedScheduleManualTrigger`, and `unifiedScheduleHelper`. The incremental,
  batch, and standalone task-scheduling tasks below are illustrative patterns
  for how a `task()` is structured; confirm the exact export before importing
  one in product code.
</Note>

### Google Calendar Sync

The platform keeps reusable Calendar synchronization helpers here, but recurring
production scheduling is handled by `apps/web` cron.

#### 1. Full Sync

Complete synchronization of all calendar events.

```typescript theme={null}
// packages/trigger/google-calendar-full-sync.ts
import { task } from "@trigger.dev/sdk/v3";

export const googleCalendarFullSync = task({
  id: "google-calendar-full-sync",
  run: async (payload: { wsId: string; userId: string }) => {
    // Implementation
  },
});
```

**Use Cases:**

* Initial calendar setup
* Recovery from sync errors
* Manual refresh requested by user

**Trigger:**

```typescript theme={null}
import { googleCalendarFullSync } from "@tuturuuu/trigger";

await googleCalendarFullSync.trigger({
  wsId: "workspace-id",
  userId: "user-id",
});
```

#### 2. Incremental Sync

Efficient delta synchronization using Google's sync tokens.

```typescript theme={null}
// packages/trigger/google-calendar-incremental-sync.ts
import { task } from "@trigger.dev/sdk/v3";

export const googleCalendarIncrementalSync = task({
  id: "google-calendar-incremental-sync",
  run: async (payload: { wsId: string; userId: string }) => {
    // Implementation using sync tokens
  },
});
```

**How It Works:**

1. Retrieves last sync token from `calendar_sync_states`
2. Fetches only changed events since last sync
3. Updates database with changes
4. Stores new sync token for next incremental sync

**Use Cases:**

* Manual or debug delta sync
* Reusable sync-token helper coverage
* Webhook-triggered updates

**Trigger:**

```typescript theme={null}
import { googleCalendarIncrementalSync } from "@tuturuuu/trigger";

await googleCalendarIncrementalSync.trigger({
  wsId: "workspace-id",
  userId: "user-id",
});
```

#### 3. Batched Sync

Batch synchronization for multiple workspaces.

```typescript theme={null}
// packages/trigger/google-calendar-sync.ts
import { task } from "@trigger.dev/sdk/v3";

export const googleCalendarBatchSync = task({
  id: "google-calendar-batch-sync",
  run: async (payload: {
    workspaces: Array<{ wsId: string; userId: string }>;
  }) => {
    // Batch processing implementation
  },
});
```

**Use Cases:**

* Admin-initiated bulk sync
* System-wide calendar refresh

**Trigger:**

```typescript theme={null}
import { googleCalendarBatchSync } from "@tuturuuu/trigger";

await googleCalendarBatchSync.trigger({
  workspaces: [
    { wsId: "ws-1", userId: "user-1" },
    { wsId: "ws-2", userId: "user-2" },
  ],
});
```

### Task Scheduling

Schedule tasks based on due dates and priorities.

```typescript theme={null}
// packages/trigger/schedule-tasks.ts
import { task } from "@trigger.dev/sdk/v3";

export const scheduleTasks = task({
  id: "schedule-tasks",
  run: async (payload: { wsId: string }) => {
    // Auto-schedule tasks into calendar
  },
});
```

**Features:**

* Analyzes task due dates and priorities
* Finds available time slots in calendar
* Creates calendar events for high-priority tasks
* Respects work hours and existing commitments

**Trigger:**

```typescript theme={null}
import { scheduleTasks } from "@tuturuuu/trigger";

await scheduleTasks.trigger({
  wsId: "workspace-id",
});
```

## Calendar Sync Implementation

### Sync Coordination

The package uses atomic sync state management to prevent concurrent syncs:

Background jobs run without a user session, so use the admin client with
`noCookie: true` (the same pattern as `packages/trigger/src`):

```typescript theme={null}
import { createAdminClient } from "@tuturuuu/supabase/next/server";

async function acquireSyncLock(wsId: string): Promise<boolean> {
  const sbAdmin = await createAdminClient({ noCookie: true });

  // Check if already syncing
  const { data: state } = await sbAdmin
    .from("calendar_sync_states")
    .select("is_syncing")
    .eq("ws_id", wsId)
    .single();

  if (state?.is_syncing) {
    return false; // Another sync in progress
  }

  // Acquire lock
  const { error } = await sbAdmin
    .from("calendar_sync_states")
    .update({ is_syncing: true })
    .eq("ws_id", wsId);

  return !error;
}

async function releaseSyncLock(wsId: string) {
  const sbAdmin = await createAdminClient({ noCookie: true });

  await sbAdmin
    .from("calendar_sync_states")
    .update({ is_syncing: false })
    .eq("ws_id", wsId);
}
```

### Full Sync Flow

```typescript theme={null}
import { task } from "@trigger.dev/sdk/v3";
import { google } from "googleapis";
import { createAdminClient } from "@tuturuuu/supabase/next/server";

export const googleCalendarFullSync = task({
  id: "google-calendar-full-sync",
  maxDuration: 300, // 5 minutes
  run: async (payload: { wsId: string; userId: string }) => {
    const sbAdmin = await createAdminClient({ noCookie: true });

    // Acquire sync lock
    const lockAcquired = await acquireSyncLock(payload.wsId);
    if (!lockAcquired) {
      throw new Error("Sync already in progress");
    }

    try {
      // Get OAuth tokens
      const { data: tokens } = await sbAdmin
        .from("calendar_auth_tokens")
        .select("*")
        .eq("user_id", payload.userId)
        .single();

      if (!tokens) throw new Error("No calendar tokens found");

      // Initialize Google Calendar API
      const oauth2Client = new google.auth.OAuth2();
      oauth2Client.setCredentials({
        access_token: tokens.access_token,
        refresh_token: tokens.refresh_token,
      });

      const calendar = google.calendar({ version: "v3", auth: oauth2Client });

      // Fetch all events
      const response = await calendar.events.list({
        calendarId: "primary",
        maxResults: 2500,
        singleEvents: true,
        orderBy: "startTime",
      });

      const events = response.data.items || [];

      // Log sync start
      const { data: syncLog } = await sbAdmin
        .from("workspace_calendar_sync_log")
        .insert({
          ws_id: payload.wsId,
          sync_type: "full",
          started_at: new Date().toISOString(),
        })
        .select()
        .single();

      // Batch upsert events
      for (let i = 0; i < events.length; i += 100) {
        const batch = events.slice(i, i + 100);

        await sbAdmin.from("workspace_calendar_events").upsert(
          batch.map((event) => ({
            ws_id: payload.wsId,
            google_event_id: event.id,
            title: event.summary,
            description: event.description,
            start_at: event.start?.dateTime || event.start?.date,
            end_at: event.end?.dateTime || event.end?.date,
            location: event.location,
            creator_id: payload.userId,
          })),
          {
            onConflict: "google_event_id",
          },
        );
      }

      // Update sync token
      await sbAdmin.from("calendar_sync_states").upsert({
        ws_id: payload.wsId,
        sync_token: response.data.nextSyncToken,
        last_synced_at: new Date().toISOString(),
      });

      // Log sync completion
      await sbAdmin
        .from("workspace_calendar_sync_log")
        .update({
          completed_at: new Date().toISOString(),
        })
        .eq("id", syncLog?.id);

      return {
        success: true,
        eventsProcessed: events.length,
      };
    } catch (error) {
      // Log error
      await sbAdmin.from("workspace_calendar_sync_log").update({
        completed_at: new Date().toISOString(),
        error: (error as Error).message,
      });

      throw error;
    } finally {
      // Always release lock
      await releaseSyncLock(payload.wsId);
    }
  },
});
```

### Incremental Sync Flow

```typescript theme={null}
import { task } from "@trigger.dev/sdk/v3";
import { google } from "googleapis";
import { createAdminClient } from "@tuturuuu/supabase/next/server";

export const googleCalendarIncrementalSync = task({
  id: "google-calendar-incremental-sync",
  maxDuration: 60, // 1 minute
  run: async (payload: { wsId: string; userId: string }) => {
    const sbAdmin = await createAdminClient({ noCookie: true });

    const lockAcquired = await acquireSyncLock(payload.wsId);
    if (!lockAcquired) return { skipped: true };

    try {
      // Get sync token
      const { data: syncState } = await sbAdmin
        .from("calendar_sync_states")
        .select("sync_token")
        .eq("ws_id", payload.wsId)
        .single();

      if (!syncState?.sync_token) {
        // No sync token - trigger full sync instead
        await googleCalendarFullSync.trigger(payload);
        return { redirectedToFullSync: true };
      }

      // Get OAuth tokens
      const { data: tokens } = await sbAdmin
        .from("calendar_auth_tokens")
        .select("*")
        .eq("user_id", payload.userId)
        .single();

      if (!tokens) throw new Error("No calendar tokens found");

      const oauth2Client = new google.auth.OAuth2();
      oauth2Client.setCredentials({
        access_token: tokens.access_token,
        refresh_token: tokens.refresh_token,
      });

      const calendar = google.calendar({ version: "v3", auth: oauth2Client });

      // Fetch changes since last sync
      const response = await calendar.events.list({
        calendarId: "primary",
        syncToken: syncState.sync_token,
      });

      const events = response.data.items || [];

      // Process changes
      for (const event of events) {
        if (event.status === "cancelled") {
          // Delete event
          await sbAdmin
            .from("workspace_calendar_events")
            .delete()
            .eq("google_event_id", event.id);
        } else {
          // Upsert event
          await sbAdmin.from("workspace_calendar_events").upsert({
            ws_id: payload.wsId,
            google_event_id: event.id,
            title: event.summary,
            description: event.description,
            start_at: event.start?.dateTime || event.start?.date,
            end_at: event.end?.dateTime || event.end?.date,
            location: event.location,
            creator_id: payload.userId,
          });
        }
      }

      // Update sync token
      await sbAdmin
        .from("calendar_sync_states")
        .update({
          sync_token: response.data.nextSyncToken,
          last_synced_at: new Date().toISOString(),
        })
        .eq("ws_id", payload.wsId);

      return {
        success: true,
        changesProcessed: events.length,
      };
    } catch (error) {
      if ((error as any).code === 410) {
        // Sync token expired - trigger full sync
        await googleCalendarFullSync.trigger(payload);
        return { syncTokenExpired: true };
      }
      throw error;
    } finally {
      await releaseSyncLock(payload.wsId);
    }
  },
});
```

## Scheduled Jobs

### Production Calendar Scheduling

Recurring Calendar work runs from `apps/web` cron, not Trigger.dev. The
canonical definitions live in `apps/web/cron.config.json`:

```json theme={null}
{
  "id": "calendar-provider-sync",
  "path": "/api/cron/calendar/provider-sync",
  "schedule": "*/15 * * * *"
},
{
  "id": "calendar-smart-schedule",
  "path": "/api/cron/calendar/smart-schedule",
  "schedule": "0 */6 * * *"
}
```

When cron definitions change, update `apps/web/cron.config.json` first and then
sync the generated `apps/web/vercel.json` cron block:

```bash theme={null}
# Fail if vercel.json is out of sync with cron.config.json (used in CI)
node scripts/sync-web-crons.js --check

# Regenerate vercel.json crons from cron.config.json
node scripts/sync-web-crons.js
```

The cron wrapper calls `/api/v1/workspaces/:wsId/calendar/sync` with cron auth
and `source: "cron"`. That workspace route owns provider fan-out, locks, and
sync dashboard audit rows. This calendar cron path stays on `apps/web` until
the TanStack/Rust migration covers recurring sync (see the warning above).

## Development

### Local Development

```bash theme={null}
# Start Trigger.dev dev server
bun trigger:dev
```

This starts a local Trigger.dev instance for manual or legacy task testing. It
is not required for production Calendar provider sync.

### Testing Jobs Locally

```typescript theme={null}
import { googleCalendarFullSync } from "@tuturuuu/trigger";

// Trigger a test run
const run = await googleCalendarFullSync.trigger({
  wsId: "test-workspace",
  userId: "test-user",
});

console.log("Run ID:", run.id);
```

## Deployment

### Deploy Jobs to Production

```bash theme={null}
bun trigger:deploy
```

This deploys Trigger.dev task wrappers for workflows that still use Trigger.dev.
Do not use it to deploy recurring Calendar provider sync or smart scheduling.

### Environment Variables

Required in `.env`:

```bash theme={null}
TRIGGER_SECRET_KEY=your_trigger_secret_key
```

## Monitoring

### View Job Runs

Access the Trigger.dev dashboard to monitor:

* Job execution history
* Success/failure rates
* Execution duration
* Error logs
* Retry attempts

### Error Handling

```typescript theme={null}
export const myJob = task({
  id: "my-job",
  retry: {
    maxAttempts: 3,
    minTimeoutInMs: 1000,
    maxTimeoutInMs: 10000,
    factor: 2,
  },
  run: async (payload) => {
    // Job implementation
  },
});
```

## Best Practices

### ✅ DO

1. **Use appropriate sync types**

   ```typescript theme={null}
   // Initial setup: Full sync
   await googleCalendarFullSync.trigger({ wsId, userId });

   // Regular updates: Incremental sync
   await googleCalendarIncrementalSync.trigger({ wsId, userId });
   ```

2. **Implement sync locks**

   ```typescript theme={null}
   const lockAcquired = await acquireSyncLock(wsId);
   if (!lockAcquired) return { skipped: true };
   ```

3. **Log sync operations**

   ```typescript theme={null}
   await sbAdmin.from("workspace_calendar_sync_log").insert({
     ws_id: wsId,
     sync_type: "incremental",
     started_at: new Date().toISOString(),
   });
   ```

4. **Set appropriate max durations**

   ```typescript theme={null}
   maxDuration: 300, // 5 minutes for full sync
   maxDuration: 60,  // 1 minute for incremental sync
   ```

5. **Handle token expiration**
   ```typescript theme={null}
   if (error.code === 410) {
     // Trigger full sync to get new token
   }
   ```

### ❌ DON'T

1. **Don't skip lock acquisition**

   ```typescript theme={null}
   // ❌ Bad: Concurrent syncs can corrupt data
   ```

2. **Don't use the user (cookie) client for background jobs**

   ```typescript theme={null}
   // ❌ Bad: cookie-bound client has no session in a background task
   const supabase = await createClient();

   // ✅ Good: admin client with noCookie for server-side jobs
   const sbAdmin = await createAdminClient({ noCookie: true });
   ```

3. **Don't ignore sync errors**
   ```typescript theme={null}
   // ❌ Bad: Silent failure
   // ✅ Good: Log to sync_log table
   ```

## Related Documentation

* [Trigger.dev Documentation](https://trigger.dev/docs)
* [Google Calendar API](https://developers.google.com/calendar/api)
* [Supabase Client](/reference/packages/supabase)

## Future Jobs

Potential background jobs to implement:

* Email processing and AI summarization
* Batch task creation from templates
* Automated report generation
* Data export and backup
* Workspace analytics calculation
