486 lines
12 KiB
Markdown
486 lines
12 KiB
Markdown
# Trigger.dev Advanced Tasks (v4)
|
|
|
|
**Advanced patterns and features for writing tasks**
|
|
|
|
## Tags & Organization
|
|
|
|
```ts
|
|
import { task, tags } from "@trigger.dev/sdk";
|
|
|
|
export const processUser = task({
|
|
id: "process-user",
|
|
run: async (payload: { userId: string; orgId: string }, { ctx }) => {
|
|
// Add tags during execution
|
|
await tags.add(`user_${payload.userId}`);
|
|
await tags.add(`org_${payload.orgId}`);
|
|
|
|
return { processed: true };
|
|
},
|
|
});
|
|
|
|
// Trigger with tags
|
|
await processUser.trigger(
|
|
{ userId: "123", orgId: "abc" },
|
|
{ tags: ["priority", "user_123", "org_abc"] } // Max 10 tags per run
|
|
);
|
|
|
|
// Subscribe to tagged runs
|
|
for await (const run of runs.subscribeToRunsWithTag("user_123")) {
|
|
console.log(`User task ${run.id}: ${run.status}`);
|
|
}
|
|
```
|
|
|
|
**Tag Best Practices:**
|
|
|
|
- Use prefixes: `user_123`, `org_abc`, `video:456`
|
|
- Max 10 tags per run, 1-64 characters each
|
|
- Tags don't propagate to child tasks automatically
|
|
|
|
## Batch Triggering v2
|
|
|
|
Enhanced batch triggering with larger payloads and streaming ingestion.
|
|
|
|
### Limits
|
|
|
|
- **Maximum batch size**: 1,000 items (increased from 500)
|
|
- **Payload per item**: 3MB each (increased from 1MB combined)
|
|
- Payloads > 512KB automatically offload to object storage
|
|
|
|
### Rate Limiting (per environment)
|
|
|
|
| Tier | Bucket Size | Refill Rate |
|
|
|------|-------------|-------------|
|
|
| Free | 1,200 runs | 100 runs/10 sec |
|
|
| Hobby | 5,000 runs | 500 runs/5 sec |
|
|
| Pro | 5,000 runs | 500 runs/5 sec |
|
|
|
|
### Concurrent Batch Processing
|
|
|
|
| Tier | Concurrent Batches |
|
|
|------|-------------------|
|
|
| Free | 1 |
|
|
| Hobby | 10 |
|
|
| Pro | 10 |
|
|
|
|
### Usage
|
|
|
|
```ts
|
|
import { myTask } from "./trigger/myTask";
|
|
|
|
// Basic batch trigger (up to 1,000 items)
|
|
const runs = await myTask.batchTrigger([
|
|
{ payload: { userId: "user-1" } },
|
|
{ payload: { userId: "user-2" } },
|
|
{ payload: { userId: "user-3" } },
|
|
]);
|
|
|
|
// Batch trigger with wait
|
|
const results = await myTask.batchTriggerAndWait([
|
|
{ payload: { userId: "user-1" } },
|
|
{ payload: { userId: "user-2" } },
|
|
]);
|
|
|
|
for (const result of results) {
|
|
if (result.ok) {
|
|
console.log("Result:", result.output);
|
|
}
|
|
}
|
|
|
|
// With per-item options
|
|
const batchHandle = await myTask.batchTrigger([
|
|
{
|
|
payload: { userId: "123" },
|
|
options: {
|
|
idempotencyKey: "user-123-batch",
|
|
tags: ["priority"],
|
|
},
|
|
},
|
|
{
|
|
payload: { userId: "456" },
|
|
options: {
|
|
idempotencyKey: "user-456-batch",
|
|
},
|
|
},
|
|
]);
|
|
```
|
|
|
|
## Debouncing
|
|
|
|
Consolidate multiple triggers into a single execution by debouncing task runs with a unique key and delay window.
|
|
|
|
### Use Cases
|
|
|
|
- **User activity updates**: Batch rapid user actions into a single run
|
|
- **Webhook deduplication**: Handle webhook bursts without redundant processing
|
|
- **Search indexing**: Combine document updates instead of processing individually
|
|
- **Notification batching**: Group notifications to prevent user spam
|
|
|
|
### Basic Usage
|
|
|
|
```ts
|
|
await myTask.trigger(
|
|
{ userId: "123" },
|
|
{
|
|
debounce: {
|
|
key: "user-123-update", // Unique identifier for debounce group
|
|
delay: "5s", // Wait duration ("5s", "1m", or milliseconds)
|
|
},
|
|
}
|
|
);
|
|
```
|
|
|
|
### Execution Modes
|
|
|
|
**Leading Mode** (default): Uses payload/options from the first trigger; subsequent triggers only reschedule execution time.
|
|
|
|
```ts
|
|
// First trigger sets the payload
|
|
await myTask.trigger({ action: "first" }, {
|
|
debounce: { key: "my-key", delay: "10s" }
|
|
});
|
|
|
|
// Second trigger only reschedules - payload remains "first"
|
|
await myTask.trigger({ action: "second" }, {
|
|
debounce: { key: "my-key", delay: "10s" }
|
|
});
|
|
// Task executes with { action: "first" }
|
|
```
|
|
|
|
**Trailing Mode**: Uses payload/options from the most recent trigger.
|
|
|
|
```ts
|
|
await myTask.trigger(
|
|
{ data: "latest-value" },
|
|
{
|
|
debounce: {
|
|
key: "trailing-example",
|
|
delay: "10s",
|
|
mode: "trailing",
|
|
},
|
|
}
|
|
);
|
|
```
|
|
|
|
In trailing mode, these options update with each trigger:
|
|
- `payload` — task input data
|
|
- `metadata` — run metadata
|
|
- `tags` — run tags (replaces existing)
|
|
- `maxAttempts` — retry attempts
|
|
- `maxDuration` — maximum compute time
|
|
- `machine` — machine preset
|
|
|
|
### Important Notes
|
|
|
|
- Idempotency keys take precedence over debounce settings
|
|
- Compatible with `triggerAndWait()` — parent runs block correctly on debounced execution
|
|
- Debounce key is scoped to the task
|
|
|
|
## Concurrency & Queues
|
|
|
|
```ts
|
|
import { task, queue } from "@trigger.dev/sdk";
|
|
|
|
// Shared queue for related tasks
|
|
const emailQueue = queue({
|
|
name: "email-processing",
|
|
concurrencyLimit: 5, // Max 5 emails processing simultaneously
|
|
});
|
|
|
|
// Task-level concurrency
|
|
export const oneAtATime = task({
|
|
id: "sequential-task",
|
|
queue: { concurrencyLimit: 1 }, // Process one at a time
|
|
run: async (payload) => {
|
|
// Critical section - only one instance runs
|
|
},
|
|
});
|
|
|
|
// Per-user concurrency
|
|
export const processUserData = task({
|
|
id: "process-user-data",
|
|
run: async (payload: { userId: string }) => {
|
|
// Override queue with user-specific concurrency
|
|
await childTask.trigger(payload, {
|
|
queue: {
|
|
name: `user-${payload.userId}`,
|
|
concurrencyLimit: 2,
|
|
},
|
|
});
|
|
},
|
|
});
|
|
|
|
export const emailTask = task({
|
|
id: "send-email",
|
|
queue: emailQueue, // Use shared queue
|
|
run: async (payload: { to: string }) => {
|
|
// Send email logic
|
|
},
|
|
});
|
|
```
|
|
|
|
## Error Handling & Retries
|
|
|
|
```ts
|
|
import { task, retry, AbortTaskRunError } from "@trigger.dev/sdk";
|
|
|
|
export const resilientTask = task({
|
|
id: "resilient-task",
|
|
retry: {
|
|
maxAttempts: 10,
|
|
factor: 1.8, // Exponential backoff multiplier
|
|
minTimeoutInMs: 500,
|
|
maxTimeoutInMs: 30_000,
|
|
randomize: false,
|
|
},
|
|
catchError: async ({ error, ctx }) => {
|
|
// Custom error handling
|
|
if (error.code === "FATAL_ERROR") {
|
|
throw new AbortTaskRunError("Cannot retry this error");
|
|
}
|
|
|
|
// Log error details
|
|
console.error(`Task ${ctx.task.id} failed:`, error);
|
|
|
|
// Allow retry by returning nothing
|
|
return { retryAt: new Date(Date.now() + 60000) }; // Retry in 1 minute
|
|
},
|
|
run: async (payload) => {
|
|
// Retry specific operations
|
|
const result = await retry.onThrow(
|
|
async () => {
|
|
return await unstableApiCall(payload);
|
|
},
|
|
{ maxAttempts: 3 }
|
|
);
|
|
|
|
// Conditional HTTP retries
|
|
const response = await retry.fetch("https://api.example.com", {
|
|
retry: {
|
|
maxAttempts: 5,
|
|
condition: (response, error) => {
|
|
return response?.status === 429 || response?.status >= 500;
|
|
},
|
|
},
|
|
});
|
|
|
|
return result;
|
|
},
|
|
});
|
|
```
|
|
|
|
## Machines & Performance
|
|
|
|
```ts
|
|
export const heavyTask = task({
|
|
id: "heavy-computation",
|
|
machine: { preset: "large-2x" }, // 8 vCPU, 16 GB RAM
|
|
maxDuration: 1800, // 30 minutes timeout
|
|
run: async (payload, { ctx }) => {
|
|
// Resource-intensive computation
|
|
if (ctx.machine.preset === "large-2x") {
|
|
// Use all available cores
|
|
return await parallelProcessing(payload);
|
|
}
|
|
|
|
return await standardProcessing(payload);
|
|
},
|
|
});
|
|
|
|
// Override machine when triggering
|
|
await heavyTask.trigger(payload, {
|
|
machine: { preset: "medium-1x" }, // Override for this run
|
|
});
|
|
```
|
|
|
|
**Machine Presets:**
|
|
|
|
- `micro`: 0.25 vCPU, 0.25 GB RAM
|
|
- `small-1x`: 0.5 vCPU, 0.5 GB RAM (default)
|
|
- `small-2x`: 1 vCPU, 1 GB RAM
|
|
- `medium-1x`: 1 vCPU, 2 GB RAM
|
|
- `medium-2x`: 2 vCPU, 4 GB RAM
|
|
- `large-1x`: 4 vCPU, 8 GB RAM
|
|
- `large-2x`: 8 vCPU, 16 GB RAM
|
|
|
|
## Idempotency
|
|
|
|
```ts
|
|
import { task, idempotencyKeys } from "@trigger.dev/sdk";
|
|
|
|
export const paymentTask = task({
|
|
id: "process-payment",
|
|
retry: {
|
|
maxAttempts: 3,
|
|
},
|
|
run: async (payload: { orderId: string; amount: number }) => {
|
|
// Automatically scoped to this task run, so if the task is retried, the idempotency key will be the same
|
|
const idempotencyKey = await idempotencyKeys.create(`payment-${payload.orderId}`);
|
|
|
|
// Ensure payment is processed only once
|
|
await chargeCustomer.trigger(payload, {
|
|
idempotencyKey,
|
|
idempotencyKeyTTL: "24h", // Key expires in 24 hours
|
|
});
|
|
},
|
|
});
|
|
|
|
// Payload-based idempotency
|
|
import { createHash } from "node:crypto";
|
|
|
|
function createPayloadHash(payload: any): string {
|
|
const hash = createHash("sha256");
|
|
hash.update(JSON.stringify(payload));
|
|
return hash.digest("hex");
|
|
}
|
|
|
|
export const deduplicatedTask = task({
|
|
id: "deduplicated-task",
|
|
run: async (payload) => {
|
|
const payloadHash = createPayloadHash(payload);
|
|
const idempotencyKey = await idempotencyKeys.create(payloadHash);
|
|
|
|
await processData.trigger(payload, { idempotencyKey });
|
|
},
|
|
});
|
|
```
|
|
|
|
## Metadata & Progress Tracking
|
|
|
|
```ts
|
|
import { task, metadata } from "@trigger.dev/sdk";
|
|
|
|
export const batchProcessor = task({
|
|
id: "batch-processor",
|
|
run: async (payload: { items: any[] }, { ctx }) => {
|
|
const totalItems = payload.items.length;
|
|
|
|
// Initialize progress metadata
|
|
metadata
|
|
.set("progress", 0)
|
|
.set("totalItems", totalItems)
|
|
.set("processedItems", 0)
|
|
.set("status", "starting");
|
|
|
|
const results = [];
|
|
|
|
for (let i = 0; i < payload.items.length; i++) {
|
|
const item = payload.items[i];
|
|
|
|
// Process item
|
|
const result = await processItem(item);
|
|
results.push(result);
|
|
|
|
// Update progress
|
|
const progress = ((i + 1) / totalItems) * 100;
|
|
metadata
|
|
.set("progress", progress)
|
|
.increment("processedItems", 1)
|
|
.append("logs", `Processed item ${i + 1}/${totalItems}`)
|
|
.set("currentItem", item.id);
|
|
}
|
|
|
|
// Final status
|
|
metadata.set("status", "completed");
|
|
|
|
return { results, totalProcessed: results.length };
|
|
},
|
|
});
|
|
|
|
// Update parent metadata from child task
|
|
export const childTask = task({
|
|
id: "child-task",
|
|
run: async (payload, { ctx }) => {
|
|
// Update parent task metadata
|
|
metadata.parent.set("childStatus", "processing");
|
|
metadata.root.increment("childrenCompleted", 1);
|
|
|
|
return { processed: true };
|
|
},
|
|
});
|
|
```
|
|
|
|
## Logging & Tracing
|
|
|
|
```ts
|
|
import { task, logger } from "@trigger.dev/sdk";
|
|
|
|
export const tracedTask = task({
|
|
id: "traced-task",
|
|
run: async (payload, { ctx }) => {
|
|
logger.info("Task started", { userId: payload.userId });
|
|
|
|
// Custom trace with attributes
|
|
const user = await logger.trace(
|
|
"fetch-user",
|
|
async (span) => {
|
|
span.setAttribute("user.id", payload.userId);
|
|
span.setAttribute("operation", "database-fetch");
|
|
|
|
const userData = await database.findUser(payload.userId);
|
|
span.setAttribute("user.found", !!userData);
|
|
|
|
return userData;
|
|
},
|
|
{ userId: payload.userId }
|
|
);
|
|
|
|
logger.debug("User fetched", { user: user.id });
|
|
|
|
try {
|
|
const result = await processUser(user);
|
|
logger.info("Processing completed", { result });
|
|
return result;
|
|
} catch (error) {
|
|
logger.error("Processing failed", {
|
|
error: error.message,
|
|
userId: payload.userId,
|
|
});
|
|
throw error;
|
|
}
|
|
},
|
|
});
|
|
```
|
|
|
|
## Hidden Tasks
|
|
|
|
```ts
|
|
// Hidden task - not exported, only used internally
|
|
const internalProcessor = task({
|
|
id: "internal-processor",
|
|
run: async (payload: { data: string }) => {
|
|
return { processed: payload.data.toUpperCase() };
|
|
},
|
|
});
|
|
|
|
// Public task that uses hidden task
|
|
export const publicWorkflow = task({
|
|
id: "public-workflow",
|
|
run: async (payload: { input: string }) => {
|
|
// Use hidden task internally
|
|
const result = await internalProcessor.triggerAndWait({
|
|
data: payload.input,
|
|
});
|
|
|
|
if (result.ok) {
|
|
return { output: result.output.processed };
|
|
}
|
|
|
|
throw new Error("Internal processing failed");
|
|
},
|
|
});
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
- **Concurrency**: Use queues to prevent overwhelming external services
|
|
- **Retries**: Configure exponential backoff for transient failures
|
|
- **Idempotency**: Always use for payment/critical operations
|
|
- **Metadata**: Track progress for long-running tasks
|
|
- **Machines**: Match machine size to computational requirements
|
|
- **Tags**: Use consistent naming patterns for filtering
|
|
- **Debouncing**: Use for user activity, webhooks, and notification batching
|
|
- **Batch triggering**: Use for bulk operations up to 1,000 items
|
|
- **Error Handling**: Distinguish between retryable and fatal errors
|
|
|
|
Design tasks to be stateless, idempotent, and resilient to failures. Use metadata for state tracking and queues for resource management.
|