From b9c54f66611ebfb1211fd27e0b2e85dc4a9f038e Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Fri, 6 Feb 2026 16:43:07 +0100 Subject: [PATCH] docs: add Trigger.dev task implementation rule (#27712) * docs: add Trigger.dev task implementation rule Co-Authored-By: eunjae@cal.com * docs: add specific file path references for CalendarsTasker and config examples Co-Authored-By: eunjae@cal.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- agents/README.md | 1 + agents/rules/patterns-trigger-dev.md | 240 +++++++++++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 agents/rules/patterns-trigger-dev.md diff --git a/agents/README.md b/agents/README.md index fdf1ff6a2c..acd66c6f1f 100644 --- a/agents/README.md +++ b/agents/README.md @@ -65,6 +65,7 @@ - [patterns-factory-pattern](rules/patterns-factory-pattern.md) - Factory pattern - [patterns-workflow-triggers](rules/patterns-workflow-triggers.md) - Workflow implementation - [patterns-app-store](rules/patterns-app-store.md) - App store integration patterns +- [patterns-trigger-dev](rules/patterns-trigger-dev.md) - Trigger.dev task implementation ### Culture diff --git a/agents/rules/patterns-trigger-dev.md b/agents/rules/patterns-trigger-dev.md new file mode 100644 index 0000000000..6730dee50c --- /dev/null +++ b/agents/rules/patterns-trigger-dev.md @@ -0,0 +1,240 @@ +--- +title: Trigger.dev Task Implementation +impact: HIGH +impactDescription: Consistent Trigger.dev patterns ensure reliable async task execution across web and API v2 +tags: trigger.dev, async, tasker, concurrency, cron, scheduled-tasks +--- + +# Trigger.dev Task Implementation + +Trigger.dev is the async task runner used in both `apps/web` and `apps/api/v2`. Tasks can be disabled via the `ENABLE_ASYNC_TASKER` env var (set to `"false"` to fall back to synchronous execution). + +## Tasker Architecture + +Every Trigger.dev feature follows the Tasker pattern with these layers: + +``` +packages/features//lib/tasker/ + types.ts # ITasker interface + payload types + Tasker.ts # Tasker subclass (dispatches async or sync) + TriggerTasker.ts # Async implementation (calls .trigger()) + SyncTasker.ts # Sync fallback (executes inline) + TaskService.ts # Business logic consumed by both + trigger/ + config.ts # Queue + retry + machine config + schema.ts # Zod payload schema + .ts # schemaTask definition +``` + +Reference implementation: `packages/features/calendars/lib/tasker/` — see `CalendarsTasker.ts` for the Tasker subclass pattern and `trigger/config.ts` for queue/retry/machine configuration + +## Creating a New Task + +### 1. Define the Zod schema for payload validation + +Always use `schemaTask` with a Zod schema for payload validation: + +```typescript +// trigger/schema.ts +import { z } from "zod"; + +export const myTaskSchema = z.object({ + userId: z.number(), +}); +``` + +### 2. Configure queue, retry, and machine + +See `packages/features/calendars/lib/tasker/trigger/config.ts` for a real example. + +```typescript +// trigger/config.ts +import { type schemaTask, queue } from "@trigger.dev/sdk"; + +type MyTask = Pick[0], "machine" | "retry" | "queue">; + +export const myQueue = queue({ + name: "my-domain", + concurrencyLimit: 10, +}); + +export const myTaskConfig: MyTask = { + machine: "small-2x", + queue: myQueue, + retry: { + maxAttempts: 3, + factor: 2, + minTimeoutInMs: 60000, + maxTimeoutInMs: 300000, + randomize: true, + outOfMemory: { + machine: "medium-1x", + }, + }, +}; +``` + +### 3. Define the task using schemaTask + +```typescript +// trigger/my-task.ts +import { schemaTask, type TaskWithSchema } from "@trigger.dev/sdk"; +import type { z } from "zod"; + +import { myTaskConfig } from "./config"; +import { myTaskSchema } from "./schema"; + +export const MY_TASK_JOB_ID = "domain.my-task"; + +export const myTask: TaskWithSchema = + schemaTask({ + id: MY_TASK_JOB_ID, + ...myTaskConfig, + schema: myTaskSchema, + run: async (payload: z.infer) => { + const { getMyService } = await import("@calcom/features//di/"); + const service = getMyService(); + await service.execute(payload); + }, + }); +``` + +### 4. For scheduled (cron) tasks, use `schedules.task` + +```typescript +import { schedules } from "@trigger.dev/sdk"; + +export const myScheduledTask = schedules.task({ + id: "domain.my-scheduled-task", + ...myTaskConfig, + cron: { + pattern: "0 0 1 * *", + timezone: "UTC", + }, + run: async (payload) => { + // task logic + }, +}); +``` + +Reference: [Trigger.dev Scheduled Tasks](https://trigger.dev/docs/tasks/scheduled) + +## Concurrency Configuration + +Concurrency limits control how many task runs execute in parallel within a queue. Getting this right is critical for production stability. + +**How to estimate concurrency:** +1. Analyze production logs for the number of requests per minute that will trigger the task (from both `apps/web` and `apps/api/v2`) +2. Measure the average execution time of the task +3. Factor in burst patterns (e.g., peak booking hours) + +**Guidelines:** +- Time-sensitive tasks (emails, webhooks) need higher concurrency +- Background jobs that are not time-sensitive can use lower concurrency +- Start conservative and increase based on production metrics + +See `packages/features/calendars/lib/tasker/trigger/config.ts` (`concurrencyLimit: 10`) and `packages/features/webhooks/lib/tasker/trigger/config.ts` (`concurrencyLimit: 20`) for real examples of how concurrency is tuned per domain. + +Reference: [Trigger.dev Concurrency & Queues](https://trigger.dev/docs/queue-concurrency) + +## Local Development + +1. Set env vars: + ``` + TRIGGER_SECRET_KEY= + TRIGGER_API_URL=https://api.trigger.dev + ENABLE_ASYNC_TASKER="true" + ``` + +2. Run the Trigger.dev CLI from the features package: + ```bash + cd packages/features && npx trigger.dev@latest dev --analyze + ``` + +3. Keep the CLI running while developing. Tasks appear in the Trigger.dev dashboard under **Tasks**. + +## Before Merging + +1. **Deploy to staging**: `cd packages/features && yarn deploy:trigger:staging` +2. **Test on Trigger.dev dashboard**: Switch to staging environment and use the **Test** tab +3. **Right-size machines**: Start with the smallest machine; increase only if you see `OutOfMemory` errors +4. **Set retry with OOM handling**: Always include `outOfMemory` in retry config with a larger machine than the default +5. **Set concurrency**: Analyze production request volume and task completion time to determine the appropriate `concurrencyLimit` +6. **Cherry-pick caveat**: Cherry-picking does not redeploy Trigger.dev tasks. Only the `draft-release` CI action does. If cherry-picking a change that impacts Trigger.dev tasks, manually promote the new version in the Trigger.dev deployment dashboard after the fix is deployed + +## Common Mistakes + +**Using `task` instead of `schemaTask`:** + +```typescript +// Bad - no payload validation +import { task } from "@trigger.dev/sdk"; +export const myTask = task({ + id: "my-task", + run: async (payload: any) => { ... }, +}); + +// Good - validated payload with Zod +import { schemaTask } from "@trigger.dev/sdk"; +export const myTask = schemaTask({ + id: "my-task", + schema: myTaskSchema, + run: async (payload) => { ... }, +}); +``` + +**Missing retry or queue config:** + +```typescript +// Bad - no retry or queue, will use defaults +export const myTask = schemaTask({ + id: "my-task", + schema: myTaskSchema, + run: async (payload) => { ... }, +}); + +// Good - explicit config from shared config file +import { myTaskConfig } from "./config"; + +export const myTask = schemaTask({ + id: "my-task", + ...myTaskConfig, + schema: myTaskSchema, + run: async (payload) => { ... }, +}); +``` + +**Importing eagerly inside task files instead of using dynamic imports:** + +```typescript +// Bad - eager import of heavy modules at file scope +import { MyService } from "@calcom/features/domain/service/MyService"; + +export const myTask = schemaTask({ + id: "my-task", + schema: myTaskSchema, + run: async (payload) => { + const service = new MyService(); + await service.execute(payload); + }, +}); + +// Good - dynamic import inside run function +export const myTask = schemaTask({ + id: "my-task", + schema: myTaskSchema, + run: async (payload) => { + const { getMyService } = await import("@calcom/features/domain/di/container"); + const service = getMyService(); + await service.execute(payload); + }, +}); +``` + +## Key References + +- Trigger.dev docs: [Concurrency & Queues](https://trigger.dev/docs/queue-concurrency), [Scheduled Tasks](https://trigger.dev/docs/tasks/scheduled), [schemaTask](https://trigger.dev/docs/tasks/schemaTask) +- Example taskers in the codebase: + - `packages/features/calendars/lib/tasker/` (standard pattern) + - `packages/features/webhooks/lib/tasker/` (webhook delivery) + - `packages/features/ee/billing/service/proration/tasker/` (scheduled cron task)