@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.
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.
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.
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.
Installation
# Already included in monorepo workspace
import { googleCalendarSync } from '@tuturuuu/trigger';
Available Jobs
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.
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.
// 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:
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.
// 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:
- Retrieves last sync token from
calendar_sync_states
- Fetches only changed events since last sync
- Updates database with changes
- Stores new sync token for next incremental sync
Use Cases:
- Manual or debug delta sync
- Reusable sync-token helper coverage
- Webhook-triggered updates
Trigger:
import { googleCalendarIncrementalSync } from "@tuturuuu/trigger";
await googleCalendarIncrementalSync.trigger({
wsId: "workspace-id",
userId: "user-id",
});
3. Batched Sync
Batch synchronization for multiple workspaces.
// 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:
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.
// 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:
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):
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
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
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:
{
"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:
# 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
# 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
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
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:
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
export const myJob = task({
id: "my-job",
retry: {
maxAttempts: 3,
minTimeoutInMs: 1000,
maxTimeoutInMs: 10000,
factor: 2,
},
run: async (payload) => {
// Job implementation
},
});
Best Practices
✅ DO
-
Use appropriate sync types
// Initial setup: Full sync
await googleCalendarFullSync.trigger({ wsId, userId });
// Regular updates: Incremental sync
await googleCalendarIncrementalSync.trigger({ wsId, userId });
-
Implement sync locks
const lockAcquired = await acquireSyncLock(wsId);
if (!lockAcquired) return { skipped: true };
-
Log sync operations
await sbAdmin.from("workspace_calendar_sync_log").insert({
ws_id: wsId,
sync_type: "incremental",
started_at: new Date().toISOString(),
});
-
Set appropriate max durations
maxDuration: 300, // 5 minutes for full sync
maxDuration: 60, // 1 minute for incremental sync
-
Handle token expiration
if (error.code === 410) {
// Trigger full sync to get new token
}
❌ DON’T
-
Don’t skip lock acquisition
// ❌ Bad: Concurrent syncs can corrupt data
-
Don’t use the user (cookie) client for background jobs
// ❌ 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 });
-
Don’t ignore sync errors
// ❌ Bad: Silent failure
// ✅ Good: Log to sync_log table
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