Skip to content

Commit b9c54f6

Browse files
docs: add Trigger.dev task implementation rule (calcom#27712)
* docs: add Trigger.dev task implementation rule Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * docs: add specific file path references for CalendarsTasker and config examples Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 8a17ebc commit b9c54f6

2 files changed

Lines changed: 241 additions & 0 deletions

File tree

agents/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
- [patterns-factory-pattern](rules/patterns-factory-pattern.md) - Factory pattern
6666
- [patterns-workflow-triggers](rules/patterns-workflow-triggers.md) - Workflow implementation
6767
- [patterns-app-store](rules/patterns-app-store.md) - App store integration patterns
68+
- [patterns-trigger-dev](rules/patterns-trigger-dev.md) - Trigger.dev task implementation
6869

6970
### Culture
7071

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
---
2+
title: Trigger.dev Task Implementation
3+
impact: HIGH
4+
impactDescription: Consistent Trigger.dev patterns ensure reliable async task execution across web and API v2
5+
tags: trigger.dev, async, tasker, concurrency, cron, scheduled-tasks
6+
---
7+
8+
# Trigger.dev Task Implementation
9+
10+
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).
11+
12+
## Tasker Architecture
13+
14+
Every Trigger.dev feature follows the Tasker pattern with these layers:
15+
16+
```
17+
packages/features/<domain>/lib/tasker/
18+
types.ts # ITasker interface + payload types
19+
<Domain>Tasker.ts # Tasker subclass (dispatches async or sync)
20+
<Domain>TriggerTasker.ts # Async implementation (calls .trigger())
21+
<Domain>SyncTasker.ts # Sync fallback (executes inline)
22+
<Domain>TaskService.ts # Business logic consumed by both
23+
trigger/
24+
config.ts # Queue + retry + machine config
25+
schema.ts # Zod payload schema
26+
<task-name>.ts # schemaTask definition
27+
```
28+
29+
Reference implementation: `packages/features/calendars/lib/tasker/` — see `CalendarsTasker.ts` for the Tasker subclass pattern and `trigger/config.ts` for queue/retry/machine configuration
30+
31+
## Creating a New Task
32+
33+
### 1. Define the Zod schema for payload validation
34+
35+
Always use `schemaTask` with a Zod schema for payload validation:
36+
37+
```typescript
38+
// trigger/schema.ts
39+
import { z } from "zod";
40+
41+
export const myTaskSchema = z.object({
42+
userId: z.number(),
43+
});
44+
```
45+
46+
### 2. Configure queue, retry, and machine
47+
48+
See `packages/features/calendars/lib/tasker/trigger/config.ts` for a real example.
49+
50+
```typescript
51+
// trigger/config.ts
52+
import { type schemaTask, queue } from "@trigger.dev/sdk";
53+
54+
type MyTask = Pick<Parameters<typeof schemaTask>[0], "machine" | "retry" | "queue">;
55+
56+
export const myQueue = queue({
57+
name: "my-domain",
58+
concurrencyLimit: 10,
59+
});
60+
61+
export const myTaskConfig: MyTask = {
62+
machine: "small-2x",
63+
queue: myQueue,
64+
retry: {
65+
maxAttempts: 3,
66+
factor: 2,
67+
minTimeoutInMs: 60000,
68+
maxTimeoutInMs: 300000,
69+
randomize: true,
70+
outOfMemory: {
71+
machine: "medium-1x",
72+
},
73+
},
74+
};
75+
```
76+
77+
### 3. Define the task using schemaTask
78+
79+
```typescript
80+
// trigger/my-task.ts
81+
import { schemaTask, type TaskWithSchema } from "@trigger.dev/sdk";
82+
import type { z } from "zod";
83+
84+
import { myTaskConfig } from "./config";
85+
import { myTaskSchema } from "./schema";
86+
87+
export const MY_TASK_JOB_ID = "domain.my-task";
88+
89+
export const myTask: TaskWithSchema<typeof MY_TASK_JOB_ID, typeof myTaskSchema> =
90+
schemaTask({
91+
id: MY_TASK_JOB_ID,
92+
...myTaskConfig,
93+
schema: myTaskSchema,
94+
run: async (payload: z.infer<typeof myTaskSchema>) => {
95+
const { getMyService } = await import("@calcom/features/<domain>/di/<container>");
96+
const service = getMyService();
97+
await service.execute(payload);
98+
},
99+
});
100+
```
101+
102+
### 4. For scheduled (cron) tasks, use `schedules.task`
103+
104+
```typescript
105+
import { schedules } from "@trigger.dev/sdk";
106+
107+
export const myScheduledTask = schedules.task({
108+
id: "domain.my-scheduled-task",
109+
...myTaskConfig,
110+
cron: {
111+
pattern: "0 0 1 * *",
112+
timezone: "UTC",
113+
},
114+
run: async (payload) => {
115+
// task logic
116+
},
117+
});
118+
```
119+
120+
Reference: [Trigger.dev Scheduled Tasks](https://trigger.dev/docs/tasks/scheduled)
121+
122+
## Concurrency Configuration
123+
124+
Concurrency limits control how many task runs execute in parallel within a queue. Getting this right is critical for production stability.
125+
126+
**How to estimate concurrency:**
127+
1. Analyze production logs for the number of requests per minute that will trigger the task (from both `apps/web` and `apps/api/v2`)
128+
2. Measure the average execution time of the task
129+
3. Factor in burst patterns (e.g., peak booking hours)
130+
131+
**Guidelines:**
132+
- Time-sensitive tasks (emails, webhooks) need higher concurrency
133+
- Background jobs that are not time-sensitive can use lower concurrency
134+
- Start conservative and increase based on production metrics
135+
136+
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.
137+
138+
Reference: [Trigger.dev Concurrency & Queues](https://trigger.dev/docs/queue-concurrency)
139+
140+
## Local Development
141+
142+
1. Set env vars:
143+
```
144+
TRIGGER_SECRET_KEY=<your-trigger-secret>
145+
TRIGGER_API_URL=https://api.trigger.dev
146+
ENABLE_ASYNC_TASKER="true"
147+
```
148+
149+
2. Run the Trigger.dev CLI from the features package:
150+
```bash
151+
cd packages/features && npx trigger.dev@latest dev --analyze
152+
```
153+
154+
3. Keep the CLI running while developing. Tasks appear in the Trigger.dev dashboard under **Tasks**.
155+
156+
## Before Merging
157+
158+
1. **Deploy to staging**: `cd packages/features && yarn deploy:trigger:staging`
159+
2. **Test on Trigger.dev dashboard**: Switch to staging environment and use the **Test** tab
160+
3. **Right-size machines**: Start with the smallest machine; increase only if you see `OutOfMemory` errors
161+
4. **Set retry with OOM handling**: Always include `outOfMemory` in retry config with a larger machine than the default
162+
5. **Set concurrency**: Analyze production request volume and task completion time to determine the appropriate `concurrencyLimit`
163+
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
164+
165+
## Common Mistakes
166+
167+
**Using `task` instead of `schemaTask`:**
168+
169+
```typescript
170+
// Bad - no payload validation
171+
import { task } from "@trigger.dev/sdk";
172+
export const myTask = task({
173+
id: "my-task",
174+
run: async (payload: any) => { ... },
175+
});
176+
177+
// Good - validated payload with Zod
178+
import { schemaTask } from "@trigger.dev/sdk";
179+
export const myTask = schemaTask({
180+
id: "my-task",
181+
schema: myTaskSchema,
182+
run: async (payload) => { ... },
183+
});
184+
```
185+
186+
**Missing retry or queue config:**
187+
188+
```typescript
189+
// Bad - no retry or queue, will use defaults
190+
export const myTask = schemaTask({
191+
id: "my-task",
192+
schema: myTaskSchema,
193+
run: async (payload) => { ... },
194+
});
195+
196+
// Good - explicit config from shared config file
197+
import { myTaskConfig } from "./config";
198+
199+
export const myTask = schemaTask({
200+
id: "my-task",
201+
...myTaskConfig,
202+
schema: myTaskSchema,
203+
run: async (payload) => { ... },
204+
});
205+
```
206+
207+
**Importing eagerly inside task files instead of using dynamic imports:**
208+
209+
```typescript
210+
// Bad - eager import of heavy modules at file scope
211+
import { MyService } from "@calcom/features/domain/service/MyService";
212+
213+
export const myTask = schemaTask({
214+
id: "my-task",
215+
schema: myTaskSchema,
216+
run: async (payload) => {
217+
const service = new MyService();
218+
await service.execute(payload);
219+
},
220+
});
221+
222+
// Good - dynamic import inside run function
223+
export const myTask = schemaTask({
224+
id: "my-task",
225+
schema: myTaskSchema,
226+
run: async (payload) => {
227+
const { getMyService } = await import("@calcom/features/domain/di/container");
228+
const service = getMyService();
229+
await service.execute(payload);
230+
},
231+
});
232+
```
233+
234+
## Key References
235+
236+
- 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)
237+
- Example taskers in the codebase:
238+
- `packages/features/calendars/lib/tasker/` (standard pattern)
239+
- `packages/features/webhooks/lib/tasker/` (webhook delivery)
240+
- `packages/features/ee/billing/service/proration/tasker/` (scheduled cron task)

0 commit comments

Comments
 (0)