agents/rules/patterns-trigger-dev.md
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).
Every Trigger.dev feature follows the Tasker pattern with these layers:
packages/features/<domain>/lib/tasker/
types.ts # ITasker interface + payload types
<Domain>Tasker.ts # Tasker subclass (dispatches async or sync)
<Domain>TriggerTasker.ts # Async implementation (calls .trigger())
<Domain>SyncTasker.ts # Sync fallback (executes inline)
<Domain>TaskService.ts # Business logic consumed by both
trigger/
config.ts # Queue + retry + machine config
schema.ts # Zod payload schema
<task-name>.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
Always use schemaTask with a Zod schema for payload validation:
// trigger/schema.ts
import { z } from "zod";
export const myTaskSchema = z.object({
userId: z.number(),
});
See packages/features/calendars/lib/tasker/trigger/config.ts for a real example.
// trigger/config.ts
import { type schemaTask, queue } from "@trigger.dev/sdk";
type MyTask = Pick<Parameters<typeof schemaTask>[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",
},
},
};
// 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<typeof MY_TASK_JOB_ID, typeof myTaskSchema> =
schemaTask({
id: MY_TASK_JOB_ID,
...myTaskConfig,
schema: myTaskSchema,
run: async (payload: z.infer<typeof myTaskSchema>) => {
const { getMyService } = await import("@calcom/features/<domain>/di/<container>");
const service = getMyService();
await service.execute(payload);
},
});
schedules.taskimport { 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
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:
apps/web and apps/api/v2)Guidelines:
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
Set env vars:
TRIGGER_SECRET_KEY=<your-trigger-secret>
TRIGGER_API_URL=https://api.trigger.dev
ENABLE_ASYNC_TASKER="true"
Run the Trigger.dev CLI from the features package:
cd packages/features && npx trigger.dev@latest dev --analyze
Keep the CLI running while developing. Tasks appear in the Trigger.dev dashboard under Tasks.
cd packages/features && yarn deploy:trigger:stagingOutOfMemory errorsoutOfMemory in retry config with a larger machine than the defaultconcurrencyLimitdraft-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 deployedUsing task instead of schemaTask:
// 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:
// 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:
// 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);
},
});
packages/features/calendars/lib/tasker/ (standard pattern)packages/features/webhooks/lib/tasker/ (webhook delivery)packages/features/ee/billing/service/proration/tasker/ (scheduled cron task)