chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:32:57 +08:00
commit cd420f9332
4811 changed files with 884702 additions and 0 deletions
@@ -0,0 +1,210 @@
---
title: "Claude Agent SDK setup guide"
sidebarTitle: "Claude Agent SDK"
icon: "sparkles"
description: "Build AI agents that can read files, run commands, and edit code using the Claude Agent SDK and Trigger.dev."
---
The [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview) gives you the same tools, agent loop, and context management that power Claude Code. Combined with Trigger.dev, you get durable execution, automatic retries, and full observability for your agents.
## Setup
<Note>
This guide assumes you are working with an existing [Trigger.dev](https://trigger.dev) project.
Follow our [quickstart](/quick-start) to get set up if you don't have a project yet.
</Note>
<Steps>
<Step title="Install the Claude Agent SDK">
```bash npm
npm install @anthropic-ai/claude-agent-sdk
```
</Step>
<Step title="Configure trigger.config.ts">
Add the SDK to the `external` array so it's not bundled:
```ts trigger.config.ts
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
project: process.env.TRIGGER_PROJECT_REF!,
build: {
external: ["@anthropic-ai/claude-agent-sdk"],
},
machine: "small-2x",
});
```
<Note>
Adding packages to `external` prevents them from being bundled, which is necessary for the Claude
Agent SDK. See the [build configuration docs](/config/config-file#external) for more details.
</Note>
</Step>
<Step title="Set your API key">
Add your Anthropic API key to your environment variables. The SDK reads it automatically.
```bash
ANTHROPIC_API_KEY=sk-ant-...
```
You can set this in the [Trigger.dev dashboard](https://cloud.trigger.dev) under **Environment Variables**, or in your `.env` file for local development.
</Step>
<Step title="Create your first agent task">
This example creates a task where Claude generates code in an empty workspace. The agent will create files based on your prompt:
```ts trigger/claude-agent.ts
import { query } from "@anthropic-ai/claude-agent-sdk";
import { schemaTask, logger } from "@trigger.dev/sdk";
import { mkdtemp, rm, readdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { z } from "zod";
export const codeGenerator = schemaTask({
id: "code-generator",
schema: z.object({
prompt: z.string(),
}),
run: async ({ prompt }, { signal }) => {
const abortController = new AbortController();
signal.addEventListener("abort", () => abortController.abort());
// Create an empty workspace for the agent
// The agent will create files here based on the prompt
const workDir = await mkdtemp(join(tmpdir(), "claude-agent-"));
logger.info("Created workspace", { workDir });
try {
const result = query({
prompt,
options: {
model: "claude-sonnet-4-20250514",
abortController,
cwd: workDir,
maxTurns: 10,
permissionMode: "acceptEdits",
allowedTools: ["Read", "Edit", "Write", "Glob"],
},
});
for await (const message of result) {
logger.info("Agent message", { type: message.type });
}
// See what files Claude created
const files = await readdir(workDir, { recursive: true });
logger.info("Files created", { files });
return { filesCreated: files };
} finally {
await rm(workDir, { recursive: true, force: true });
}
},
});
```
</Step>
<Step title="Run the dev server">
```bash
npx trigger.dev@latest dev
```
</Step>
<Step title="Test your agent">
Go to the Trigger.dev dashboard, find your `code-generator` task, and trigger it with a test payload:
```json
{
"prompt": "Create a Node.js project with a fibonacci.ts file containing a function to calculate fibonacci numbers, and a fibonacci.test.ts file with tests."
}
```
</Step>
</Steps>
## How it works
The `query()` function runs Claude in an agentic loop where it can:
1. **Read files** - Explore codebases with `Read`, `Grep`, and `Glob` tools
2. **Edit files** - Modify code with `Edit` and `Write` tools
3. **Run commands** - Execute shell commands with `Bash` tool (if enabled)
4. **Think step by step** - Use extended thinking for complex problems
The agent continues until it completes the task or reaches `maxTurns`.
### Permission modes
| Mode | What it does |
| --------------------- | ----------------------------------------------------- |
| `"default"` | Asks for approval on potentially dangerous operations |
| `"acceptEdits"` | Auto-approves file operations, asks for bash/network |
| `"bypassPermissions"` | Skips all safety checks (not recommended) |
### Available tools
```ts
allowedTools: [
"Task", // Planning and task management
"Glob", // Find files by pattern
"Grep", // Search file contents
"Read", // Read file contents
"Edit", // Edit existing files
"Write", // Create new files
"Bash", // Run shell commands
"TodoRead", // Read todo list
"TodoWrite", // Update todo list
];
```
## GitHub repo
<Card
title="View the Claude Agent SDK + Trigger.dev example"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/claude-agent-sdk-trigger"
>
A complete example with two agent patterns: basic safe code generation and advanced with bash
execution.
</Card>
## Example projects using the Claude Agent SDK
<CardGroup cols={2}>
<Card
title="Claude changelog generator"
icon="scroll"
href="/guides/example-projects/claude-changelog-generator"
>
Generate changelogs from git commits using custom MCP tools.
</Card>
<Card
title="Claude GitHub wiki agent"
icon="book"
href="/guides/example-projects/claude-github-wiki"
>
Analyze repositories and answer questions with real-time streaming.
</Card>
</CardGroup>
## Learn more
- [Claude Agent SDK docs](https://platform.claude.com/docs/en/agent-sdk/overview) Official Anthropic documentation
- [Trigger.dev Realtime](/realtime/overview) Stream agent progress to your frontend
- [Waitpoints](/wait) Add human-in-the-loop approval steps
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,120 @@
---
title: "Generate and translate copy"
sidebarTitle: "Generate & translate copy"
description: "Create an AI agent workflow that generates and translates copy"
---
## Overview
**Prompt chaining** is an AI workflow pattern that decomposes a complex task into a sequence of steps, where each LLM call processes the output of the previous one. This approach trades off latency for higher accuracy by making each LLM call an easier, more focused task, with the ability to add programmatic checks between steps to ensure the process remains on track.
![Generating and translating copy](/guides/ai-agents/prompt-chaining.png)
## Example task
In this example, we'll create a workflow that generates and translates copy. This approach is particularly effective when tasks require different models or approaches for different inputs.
**This task:**
- Uses `generateText` from [Vercel's AI SDK](https://sdk.vercel.ai/docs/introduction) to interact with OpenAI models
- Uses `experimental_telemetry` to provide LLM logs
- Generates marketing copy based on subject and target word count
- Validates the generated copy meets word count requirements (±10 words)
- Translates the validated copy to the target language while preserving tone
```typescript
import { openai } from "@ai-sdk/openai";
import { task } from "@trigger.dev/sdk";
import { generateText } from "ai";
export interface TranslatePayload {
marketingSubject: string;
targetLanguage: string;
targetWordCount: number;
}
export const generateAndTranslateTask = task({
id: "generate-and-translate-copy",
maxDuration: 300, // Stop executing after 5 mins of compute
run: async (payload: TranslatePayload) => {
// Step 1: Generate marketing copy
const generatedCopy = await generateText({
model: openai("o1-mini"),
messages: [
{
role: "system",
content: "You are an expert copywriter.",
},
{
role: "user",
content: `Generate as close as possible to ${payload.targetWordCount} words of compelling marketing copy for ${payload.marketingSubject}`,
},
],
experimental_telemetry: {
isEnabled: true,
functionId: "generate-and-translate-copy",
},
});
// Gate: Validate the generated copy meets the word count target
const wordCount = generatedCopy.text.split(/\s+/).length;
if (
wordCount < payload.targetWordCount - 10 ||
wordCount > payload.targetWordCount + 10
) {
throw new Error(
`Generated copy length (${wordCount} words) is outside acceptable range of ${
payload.targetWordCount - 10
}-${payload.targetWordCount + 10} words`
);
}
// Step 2: Translate to target language
const translatedCopy = await generateText({
model: openai("o1-mini"),
messages: [
{
role: "system",
content: `You are an expert translator specializing in marketing content translation into ${payload.targetLanguage}.`,
},
{
role: "user",
content: `Translate the following marketing copy to ${payload.targetLanguage}, maintaining the same tone and marketing impact:\n\n${generatedCopy.text}`,
},
],
experimental_telemetry: {
isEnabled: true,
functionId: "generate-and-translate-copy",
},
});
return {
englishCopy: generatedCopy,
translatedCopy,
};
},
});
```
## Run a test
On the Test page in the dashboard, select the `generate-and-translate-copy` task and include a payload like the following:
```json
{
marketingSubject: "The controversial new Jaguar electric concept car",
targetLanguage: "Spanish",
targetWordCount: 100,
}
```
This example payload generates copy and then translates it using sequential LLM calls. The translation only begins after the generated copy has been validated against the word count requirements.
<video
src="https://content.trigger.dev/agent-prompt-chaining-3.mp4"
controls
muted
autoPlay
loop
/>
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+107
View File
@@ -0,0 +1,107 @@
---
title: "AI agents overview"
sidebarTitle: "Overview"
description: "Real world AI agent example tasks using Trigger.dev"
---
## Example projects using AI agents
<CardGroup cols={2}>
<Card
icon="scroll"
title="Claude changelog generator"
href="/guides/example-projects/claude-changelog-generator"
>
Automatically generate professional changelogs from git commits using Claude.
</Card>
<Card
icon="book"
title="Claude GitHub wiki agent"
href="/guides/example-projects/claude-github-wiki"
>
Generate and maintain GitHub wiki documentation with Claude-powered analysis.
</Card>
<Card
icon="hand"
title="Human-in-the-loop workflow"
href="/guides/example-projects/human-in-the-loop-workflow"
>
Create audio summaries of newspaper articles using a human-in-the-loop workflow built with
ReactFlow and Trigger.dev waitpoint tokens.
</Card>
<Card
title="Mastra agents with memory"
icon="database"
href="/guides/example-projects/mastra-agents-with-memory"
>
Use Mastra to create a weather agent that can collect live weather data and generate clothing
recommendations.
</Card>
<Card
title="OpenAI Agent Python SDK guardrails"
icon="snake"
href="/guides/example-projects/openai-agent-sdk-guardrails"
>
Use the OpenAI Agent SDK to create a guardrails system for your AI agents.
</Card>
<Card
title="OpenAI Agent TypeScript SDK playground"
icon="rocket"
href="/guides/example-projects/openai-agents-sdk-typescript-playground"
>
A playground containing 7 AI agents using the OpenAI Agent SDK for TypeScript with Trigger.dev.
</Card>
<Card
title="Vercel AI SDK deep research agent"
icon="triangle"
href="/guides/example-projects/vercel-ai-sdk-deep-research"
>
Use the Vercel AI SDK to generate comprehensive PDF reports using a deep research agent.
</Card>
<Card
title="Smart Spreadsheet"
icon="table"
href="/guides/example-projects/smart-spreadsheet"
>
Enrich company data using Exa search and Claude with real-time streaming results.
</Card>
</CardGroup>
## Agent fundamentals
These guides will show you how to set up different types of AI agent workflows with Trigger.dev. The examples take inspiration from Anthropic's blog post on [building effective agents](https://www.anthropic.com/research/building-effective-agents).
<CardGroup cols={2}>
<Card
title="Prompt chaining"
img="/guides/ai-agents/prompt-chaining.png"
href="/guides/ai-agents/generate-translate-copy"
>
Chain prompts together to generate and translate marketing copy automatically
</Card>
<Card title="Routing" img="/guides/ai-agents/routing.png" href="/guides/ai-agents/route-question">
Send questions to different AI models based on complexity analysis
</Card>
<Card
title="Parallelization"
img="/guides/ai-agents/parallelization.png"
href="/guides/ai-agents/respond-and-check-content"
>
Simultaneously check for inappropriate content while responding to customer inquiries
</Card>
<Card
title="Orchestrator"
img="/guides/ai-agents/orchestrator-workers.png"
href="/guides/ai-agents/verify-news-article"
>
Coordinate multiple AI workers to verify news article accuracy
</Card>
<Card
title="Evaluator-optimizer"
img="/guides/ai-agents/evaluator-optimizer.png"
href="/guides/ai-agents/translate-and-refine"
>
Translate text and automatically improve quality through feedback loops
</Card>
</CardGroup>
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,134 @@
---
title: "Respond to customer inquiry and check for inappropriate content"
sidebarTitle: "Respond & check content"
description: "Create an AI agent workflow that responds to customer inquiries while checking if their text is inappropriate"
---
## Overview
**Parallelization** is a workflow pattern where multiple tasks or processes run simultaneously instead of sequentially, allowing for more efficient use of resources and faster overall execution. It's particularly valuable when different parts of a task can be handled independently, such as running content analysis and response generation at the same time.
![Parallelization](/guides/ai-agents/parallelization.png)
## Example task
In this example, we'll create a workflow that simultaneously checks content for issues while responding to customer inquiries. This approach is particularly effective when tasks require multiple perspectives or parallel processing streams, with the orchestrator synthesizing the results into a cohesive output.
**This task:**
- Uses `generateText` from [Vercel's AI SDK](https://sdk.vercel.ai/docs/introduction) to interact with OpenAI models
- Uses `experimental_telemetry` to provide LLM logs
- Uses [`batch.triggerByTaskAndWait`](/triggering#batch-triggerbytaskandwait) to run customer response and content moderation tasks in parallel
- Generates customer service responses using an AI model
- Simultaneously checks for inappropriate content while generating responses
```typescript
import { openai } from "@ai-sdk/openai";
import { batch, task } from "@trigger.dev/sdk";
import { generateText } from "ai";
// Task to generate customer response
export const generateCustomerResponse = task({
id: "generate-customer-response",
run: async (payload: { question: string }) => {
const response = await generateText({
model: openai("o1-mini"),
messages: [
{
role: "system",
content: "You are a helpful customer service representative.",
},
{ role: "user", content: payload.question },
],
experimental_telemetry: {
isEnabled: true,
functionId: "generate-customer-response",
},
});
return response.text;
},
});
// Task to check for inappropriate content
export const checkInappropriateContent = task({
id: "check-inappropriate-content",
run: async (payload: { text: string }) => {
const response = await generateText({
model: openai("o1-mini"),
messages: [
{
role: "system",
content:
"You are a content moderator. Respond with 'true' if the content is inappropriate or contains harmful, threatening, offensive, or explicit content, 'false' otherwise.",
},
{ role: "user", content: payload.text },
],
experimental_telemetry: {
isEnabled: true,
functionId: "check-inappropriate-content",
},
});
return response.text.toLowerCase().includes("true");
},
});
// Main task that coordinates the parallel execution
export const handleCustomerQuestion = task({
id: "handle-customer-question",
run: async (payload: { question: string }) => {
const {
runs: [responseRun, moderationRun],
} = await batch.triggerByTaskAndWait([
{
task: generateCustomerResponse,
payload: { question: payload.question },
},
{
task: checkInappropriateContent,
payload: { text: payload.question },
},
]);
// Check moderation result first
if (moderationRun.ok && moderationRun.output === true) {
return {
response:
"I apologize, but I cannot process this request as it contains inappropriate content.",
wasInappropriate: true,
};
}
// Return the generated response if everything is ok
if (responseRun.ok) {
return {
response: responseRun.output,
wasInappropriate: false,
};
}
// Handle any errors
throw new Error("Failed to process customer question");
},
});
```
## Run a test
On the Test page in the dashboard, select the `handle-customer-question` task and include a payload like the following:
``` json
{
"question": "Can you explain 2FA?"
}
```
When triggered with a question, the task simultaneously generates a response while checking for inappropriate content using two parallel LLM calls. The main task waits for both operations to complete before delivering the final response.
<video
src="https://content.trigger.dev/agent-parallelization.mp4"
controls
muted
autoPlay
loop
/>
+114
View File
@@ -0,0 +1,114 @@
---
title: "Route a question to a different AI model"
sidebarTitle: "Route a question"
description: "Create an AI agent workflow that routes a question to a different AI model depending on its complexity"
---
## Overview
**Routing** is a workflow pattern that classifies an input and directs it to a specialized followup task. This pattern allows for separation of concerns and building more specialized prompts, which is particularly effective when there are distinct categories that are better handled separately. Without routing, optimizing for one kind of input can hurt performance on other inputs.
![Routing](/guides/ai-agents/routing.png)
## Example task
In this example, we'll create a workflow that routes a question to a different AI model depending on its complexity. This approach is particularly effective when tasks require different models or approaches for different inputs.
**This task:**
- Uses `generateText` from [Vercel's AI SDK](https://sdk.vercel.ai/docs/introduction) to interact with OpenAI models
- Uses `experimental_telemetry` in the source verification and historical analysis tasks to provide LLM logs
- Routes questions using a lightweight model (`o1-mini`) to classify complexity
- Directs simple questions to `gpt-4o` and complex ones to `gpt-o3-mini`
- Returns both the answer and metadata about the routing decision
```typescript
import { openai } from "@ai-sdk/openai";
import { task } from "@trigger.dev/sdk";
import { generateText } from "ai";
import { z } from "zod";
// Schema for router response
const routingSchema = z.object({
model: z.enum(["gpt-4o", "gpt-o3-mini"]),
reason: z.string(),
});
// Router prompt template
const ROUTER_PROMPT = `You are a routing assistant that determines the complexity of questions.
Analyze the following question and route it to the appropriate model:
- Use "gpt-4o" for simple, common, or straightforward questions
- Use "gpt-o3-mini" for complex, unusual, or questions requiring deep reasoning
Respond with a JSON object in this exact format:
{"model": "gpt-4o" or "gpt-o3-mini", "reason": "your reasoning here"}
Question: `;
export const routeAndAnswerQuestion = task({
id: "route-and-answer-question",
run: async (payload: { question: string }) => {
// Step 1: Route the question
const routingResponse = await generateText({
model: openai("o1-mini"),
messages: [
{
role: "system",
content:
"You must respond with a valid JSON object containing only 'model' and 'reason' fields. No markdown, no backticks, no explanation.",
},
{
role: "user",
content: ROUTER_PROMPT + payload.question,
},
],
temperature: 0.1,
experimental_telemetry: {
isEnabled: true,
functionId: "route-and-answer-question",
},
});
// Add error handling and cleanup
let jsonText = routingResponse.text.trim();
if (jsonText.startsWith("```")) {
jsonText = jsonText.replace(/```json\n|\n```/g, "");
}
const routingResult = routingSchema.parse(JSON.parse(jsonText));
// Step 2: Get the answer using the selected model
const answerResult = await generateText({
model: openai(routingResult.model),
messages: [{ role: "user", content: payload.question }],
});
return {
answer: answerResult.text,
selectedModel: routingResult.model,
routingReason: routingResult.reason,
};
},
});
```
## Run a test
Triggering our task with a simple question shows it routing to the gpt-4o model and returning the answer with reasoning:
```json
{
"question": "How many planets are there in the solar system?"
}
```
<video
src="https://content.trigger.dev/agent-routing.mp4"
controls
muted
autoPlay
loop
/>
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@@ -0,0 +1,170 @@
---
title: "Translate text and refine it based on feedback"
sidebarTitle: "Translate and refine"
description: "This guide will show you how to create a task that translates text and refines it based on feedback."
---
## Overview
This example is based on the **evaluator-optimizer** pattern, where one LLM generates a response while another provides evaluation and feedback in a loop. This is particularly effective for tasks with clear evaluation criteria where iterative refinement provides better results.
![Evaluator-optimizer](/guides/ai-agents/evaluator-optimizer.png)
## Example task
This example task translates text into a target language and refines the translation over a number of iterations based on feedback provided by the LLM.
**This task:**
- Uses `generateText` from [Vercel's AI SDK](https://sdk.vercel.ai/docs/introduction) to generate the translation
- Uses `experimental_telemetry` to provide LLM logs on the Run page in the dashboard
- Runs for a maximum of 10 iterations
- Uses `generateText` again to evaluate the translation
- Recursively calls itself to refine the translation based on the feedback
```typescript
import { task } from "@trigger.dev/sdk";
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
interface TranslationPayload {
text: string;
targetLanguage: string;
previousTranslation?: string;
feedback?: string;
rejectionCount?: number;
}
export const translateAndRefine = task({
id: "translate-and-refine",
run: async (payload: TranslationPayload) => {
const rejectionCount = payload.rejectionCount || 0;
// Bail out if we've hit the maximum attempts
if (rejectionCount >= 10) {
return {
finalTranslation: payload.previousTranslation,
iterations: rejectionCount,
status: "MAX_ITERATIONS_REACHED",
};
}
// Generate translation (or refinement if we have previous feedback)
const translationPrompt = payload.feedback
? `Previous translation: "${payload.previousTranslation}"\n\nFeedback received: "${payload.feedback}"\n\nPlease provide an improved translation addressing this feedback.`
: `Translate this text into ${payload.targetLanguage}, preserving style and meaning: "${payload.text}"`;
const translation = await generateText({
model: openai("o1-mini"),
messages: [
{
role: "system",
content: `You are an expert literary translator into ${payload.targetLanguage}.
Focus on accuracy first, then style and natural flow.`,
},
{
role: "user",
content: translationPrompt,
},
],
experimental_telemetry: {
isEnabled: true,
functionId: "translate-and-refine",
},
});
// Evaluate the translation
const evaluation = await generateText({
model: openai("o1-mini"),
messages: [
{
role: "system",
content: `You are an expert literary critic and translator focused on practical, high-quality translations.
Your goal is to ensure translations are accurate and natural, but not necessarily perfect.
This is iteration ${
rejectionCount + 1
} of a maximum 5 iterations.
RESPONSE FORMAT:
- If the translation meets 90%+ quality: Respond with exactly "APPROVED" (nothing else)
- If improvements are needed: Provide only the specific issues that must be fixed
Evaluation criteria:
- Accuracy of meaning (primary importance)
- Natural flow in the target language
- Preservation of key style elements
DO NOT provide detailed analysis, suggestions, or compliments.
DO NOT include the translation in your response.
IMPORTANT RULES:
- First iteration MUST receive feedback for improvement
- Be very strict on accuracy in early iterations
- After 3 iterations, lower quality threshold to 85%`,
},
{
role: "user",
content: `Original: "${payload.text}"
Translation: "${translation.text}"
Target Language: ${payload.targetLanguage}
Iteration: ${rejectionCount + 1}
Previous Feedback: ${
payload.feedback ? `"${payload.feedback}"` : "None"
}
${
rejectionCount === 0
? "This is the first attempt. Find aspects to improve."
: 'Either respond with exactly "APPROVED" or provide only critical issues that must be fixed.'
}`,
},
],
experimental_telemetry: {
isEnabled: true,
functionId: "translate-and-refine",
},
});
// If approved, return the final result
if (evaluation.text.trim() === "APPROVED") {
return {
finalTranslation: translation.text,
iterations: rejectionCount,
status: "APPROVED",
};
}
// If not approved, recursively call the task with feedback
await translateAndRefine
.triggerAndWait({
text: payload.text,
targetLanguage: payload.targetLanguage,
previousTranslation: translation.text,
feedback: evaluation.text,
rejectionCount: rejectionCount + 1,
})
.unwrap();
},
});
```
## Run a test
On the Test page in the dashboard, select the `translate-and-refine` task and include a payload like the following:
```json
{
"text": "In the twilight of his years, the old clockmaker's hands, once steady as the timepieces he crafted, now trembled like autumn leaves in the wind.",
"targetLanguage": "French"
}
```
This example payload translates the text into French and should be suitably difficult to require a few iterations, depending on the model used and the prompt criteria you set.
<video
src="https://content.trigger.dev/agent-evaluator-optimizer.mp4"
controls
muted
autoPlay
loop
/>
@@ -0,0 +1,220 @@
---
title: "Verify a news article"
sidebarTitle: "Verify news article"
description: "Create an AI agent workflow that verifies the facts in a news article"
---
## Overview
This example demonstrates the **orchestrator-workers** pattern, where a central AI agent dynamically breaks down complex tasks and delegates them to specialized worker agents. This pattern is particularly effective when tasks require multiple perspectives or parallel processing streams, with the orchestrator synthesizing the results into a cohesive output.
![Orchestrator](/guides/ai-agents/orchestrator-workers.png)
## Example task
Our example task uses multiple LLM calls to extract claims from a news article and analyze them in parallel, combining source verification and historical context to assess their credibility.
**This task:**
- Uses `generateText` from [Vercel's AI SDK](https://sdk.vercel.ai/docs/introduction) to interact with OpenAI models
- Uses `experimental_telemetry` to provide LLM logs
- Uses [`batch.triggerByTaskAndWait`](/triggering#batch-triggerbytaskandwait) to orchestrate parallel processing of claims
- Extracts factual claims from news articles using the `o1-mini` model
- Evaluates claims against recent sources and analyzes historical context in parallel
- Combines results into a structured analysis report
```typescript
import { openai } from "@ai-sdk/openai";
import { batch, logger, task } from "@trigger.dev/sdk";
import { CoreMessage, generateText } from "ai";
// Define types for our workers' outputs
interface Claim {
id: number;
text: string;
}
interface SourceVerification {
claimId: number;
isVerified: boolean;
confidence: number;
explanation: string;
}
interface HistoricalAnalysis {
claimId: number;
feasibility: number;
historicalContext: string;
}
// Worker 1: Claim Extractor
export const extractClaims = task({
id: "extract-claims",
run: async ({ article }: { article: string }) => {
try {
const messages: CoreMessage[] = [
{
role: "system",
content:
"Extract distinct factual claims from the news article. Format as numbered claims.",
},
{
role: "user",
content: article,
},
];
const response = await generateText({
model: openai("o1-mini"),
messages,
});
const claims = response.text
.split("\n")
.filter((line: string) => line.trim())
.map((claim: string, index: number) => ({
id: index + 1,
text: claim.replace(/^\d+\.\s*/, ""),
}));
logger.info("Extracted claims", { claimCount: claims.length });
return claims;
} catch (error) {
logger.error("Error in claim extraction", {
error: error instanceof Error ? error.message : "Unknown error",
});
throw error;
}
},
});
// Worker 2: Source Verifier
export const verifySource = task({
id: "verify-source",
run: async (claim: Claim) => {
const response = await generateText({
model: openai("o1-mini"),
messages: [
{
role: "system",
content:
"Verify this claim by considering recent news sources and official statements. Assess reliability.",
},
{
role: "user",
content: claim.text,
},
],
experimental_telemetry: {
isEnabled: true,
functionId: "verify-source",
},
});
return {
claimId: claim.id,
isVerified: false,
confidence: 0.7,
explanation: response.text,
};
},
});
// Worker 3: Historical Context Analyzer
export const analyzeHistory = task({
id: "analyze-history",
run: async (claim: Claim) => {
const response = await generateText({
model: openai("o1-mini"),
messages: [
{
role: "system",
content:
"Analyze this claim in historical context, considering past announcements and technological feasibility.",
},
{
role: "user",
content: claim.text,
},
],
experimental_telemetry: {
isEnabled: true,
functionId: "analyze-history",
},
});
return {
claimId: claim.id,
feasibility: 0.8,
historicalContext: response.text,
};
},
});
// Orchestrator
export const newsFactChecker = task({
id: "news-fact-checker",
run: async ({ article }: { article: string }) => {
// Step 1: Extract claims
const claimsResult = await batch.triggerByTaskAndWait([
{ task: extractClaims, payload: { article } },
]);
if (!claimsResult.runs[0].ok) {
logger.error("Failed to extract claims", {
error: claimsResult.runs[0].error,
runId: claimsResult.runs[0].id,
});
throw new Error(
`Failed to extract claims: ${claimsResult.runs[0].error}`
);
}
const claims = claimsResult.runs[0].output;
// Step 2: Process claims in parallel
const parallelResults = await batch.triggerByTaskAndWait([
...claims.map((claim) => ({ task: verifySource, payload: claim })),
...claims.map((claim) => ({ task: analyzeHistory, payload: claim })),
]);
// Split and process results
const verifications = parallelResults.runs
.filter(
(run): run is typeof run & { ok: true } =>
run.ok && run.taskIdentifier === "verify-source"
)
.map((run) => run.output as SourceVerification);
const historicalAnalyses = parallelResults.runs
.filter(
(run): run is typeof run & { ok: true } =>
run.ok && run.taskIdentifier === "analyze-history"
)
.map((run) => run.output as HistoricalAnalysis);
return { claims, verifications, historicalAnalyses };
},
});
```
## Run a test
On the Test page in the dashboard, select the `news-fact-checker` task and include a payload like the following:
```json
{
"article": "Tesla announced a new breakthrough in battery technology today. The company claims their new batteries will have 50% more capacity and cost 30% less to produce. Elon Musk stated this development will enable electric vehicles to achieve price parity with gasoline cars by 2024. The new batteries are scheduled to enter production next quarter at the Texas Gigafactory."
}
```
This example payload verifies the claims in the news article and provides a report on the results.
<video
src="https://content.trigger.dev/agent-orchestrator-workers.mp4"
controls
muted
autoPlay
loop
/>
+10
View File
@@ -0,0 +1,10 @@
---
title: "dotenvx"
sidebarTitle: "dotenvx"
description: "A dotenvx package for Trigger.dev."
icon: "square-e"
---
This is a community developed package from [dotenvx](https://dotenvx.com/) that enables you to use dotenvx with Trigger.dev.
[View the docs](https://dotenvx.com/docs/background-jobs/triggerdotdev)
+12
View File
@@ -0,0 +1,12 @@
---
title: "Fatima"
sidebarTitle: "Fatima"
description: "A Fatima package for Trigger.dev."
icon: "f"
---
This is a community developed package from [@Fgc17](https://github.com/Fgc17) that enables you to use Fatima with Trigger.dev.
[View the Fatima docs](https://fatimajs.vercel.app/docs/adapters/trigger)
[View the repo](https://github.com/Fgc17/fatima)
+10
View File
@@ -0,0 +1,10 @@
---
title: "Rate limiter"
sidebarTitle: "Rate limiter"
description: "A rate limiter for Trigger.dev."
icon: "gauge-simple-low"
---
This is a community developed package from [@ian](https://github.com/ian) that uses Redis to rate limit Trigger.dev tasks.
[View the repo](https://github.com/ian/trigger-rate-limiting)
+30
View File
@@ -0,0 +1,30 @@
---
title: "SvelteKit setup guide"
sidebarTitle: "SvelteKit"
description: "A plugin for SvelteKit to integrate with Trigger.dev."
icon: "s"
---
import Prerequisites from "/snippets/framework-prerequisites.mdx";
This is a community developed Vite plugin from [@cptCrunch_](https://x.com/cptCrunch_) that enables seamless integration between SvelteKit and Trigger.dev by allowing you to use your SvelteKit functions directly in your Trigger.dev projects.
## Features
- Use SvelteKit functions directly in Trigger.dev tasks
- Automatic function discovery and export
- TypeScript support with type preservation
- Works with Trigger.dev V3
- Configurable directory scanning
<Prerequisites framework="SvelteKit" />
## Setup
[View setup guide on npm](https://www.npmjs.com/package/triggerkit)
```bash
npm i triggerkit
```
<UsefulNextSteps />
+51
View File
@@ -0,0 +1,51 @@
---
title: "Using Cursor with Trigger.dev"
sidebarTitle: "Cursor rules"
description: "This guide shows how to add Cursor rules to a project to help you write Trigger.dev tasks faster and more accurately."
icon: "hexagon"
---
## Overview
[Cursor](https://www.cursor.com/) is a powerful AI coding editor which understands your codebase and can help you write code faster and more accurately. This guide shows you how to add our Cursor rules to your project to help you write Trigger.dev tasks.
## Prerequisites
- [Cursor](https://www.cursor.com/) installed on your machine
## Installing our Cursor rules
1. Locate the `.cursor/rules` folder in your project root, or click the "add new rule" button in `Cursor Settings` > `Rules`.
2. Download the [writing-tasks.mdc](https://github.com/triggerdotdev/trigger.dev/blob/main/.cursor/rules/writing-tasks.mdc) file and place it in the `.cursor/rules` folder
3. For more help installing Cursor rules, see [Cursor's docs](https://docs.cursor.com/context/rules-for-ai)
<video
src="https://content.trigger.dev/cursor-rules-write-task.mp4"
preload="auto"
controls={true}
loop
muted
autoPlay={true}
width="100%"
height="100%"
/>
| <span style={{ color: '#28BF5C' }}>Helps with</span> | <span style={{ color: '#E11D48' }}>Don't use it for</span> |
| ---------------------------------------------------- | ---------------------------------------------------------- |
| Creating basic tasks | Deploying |
| Creating scheduled tasks | Project setup |
| Creating schema tasks | Infrastructure configuration |
| Triggering tasks from backend | API keys |
| Task lifecycle functions | Using tags |
| Using Realtime | |
| Idempotency patterns | |
| Metadata handling | |
| Using build extensions | |
## Tips for making the most of Cursor
1. **Turn on auto-run** in Cursor settings. This allows Cursor to act more like an agent that checks for errors, fixes them, runs commands to install packages, and it won't stop until the code is error-free and running. _NB: Use this mode with caution, as it can make changes without your approval._
2. **Use version control like GitHub or GitLab**, and commit frequently, so you can roll-back if something changes unexpectedly. Cursor can make changes to multiple files at once, so it's best to commit often to keep track of all your changes.
3. **Explicitly add context when needed** - Cursor should know when to use the rules file, but I recommend selecting it from the "@ Add context" button above the prompt window. Then select "Cursor Rules" and select your rules file.
@@ -0,0 +1,124 @@
---
title: "Automated website monitoring with Anchor Browser"
sidebarTitle: "Anchor Browser web scraper"
description: "Automated web monitoring using Trigger.dev's task scheduling and Anchor Browser's AI-powered browser automation."
---
import WebScrapingWarning from "/snippets/web-scraping-warning.mdx";
<WebScrapingWarning />
## Overview
This example demonstrates automated web monitoring using Trigger.dev's task scheduling and Anchor Browser's AI-powered browser automation tools.
The task runs daily at 5pm ET to find the cheapest Broadway tickets available for same-day shows.
**How it works:**
- Trigger.dev schedules and executes the monitoring task
- Anchor Browser spins up a remote browser session with an AI agent
- The AI agent uses computer vision and natural language processing to analyze the TDF website
- AI agent returns the lowest-priced show with specific details: name, price, and showtime
## Tech stack
- **[Node.js](https://nodejs.org)** runtime environment (version 18.2 or higher)
- **[Trigger.dev](https://trigger.dev)** for task scheduling and task orchestration
- **[Anchor Browser](https://anchorbrowser.io/)** for AI-powered browser automation
- **[Playwright](https://playwright.dev/)** for browser automation libraries (handled via external dependencies)
## GitHub repo
<Card
title="View the Anchor Browser web scraper repo"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/anchor-browser-web-scraper"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## Relevant code
### Broadway ticket monitor task
This task runs daily at 5pm ET, in [src/trigger/broadway-monitor.ts](https://github.com/triggerdotdev/examples/tree/main/anchor-browser-web-scraper/src/trigger/broadway-monitor.ts):
```ts
import { schedules } from "@trigger.dev/sdk";
import Anchorbrowser from "anchorbrowser";
export const broadwayMonitor = schedules.task({
id: "broadway-ticket-monitor",
cron: "0 21 * * *",
run: async (payload, { ctx }) => {
const client = new Anchorbrowser({
apiKey: process.env.ANCHOR_BROWSER_API_KEY!,
});
let session;
try {
// Create explicit session to get live view URL
session = await client.sessions.create();
console.log(`Session ID: ${session.data.id}`);
console.log(`Live View URL: https://live.anchorbrowser.io?sessionId=${session.data.id}`);
const response = await client.tools.performWebTask({
sessionId: session.data.id,
url: "https://www.tdf.org/discount-ticket-programs/tkts-by-tdf/tkts-live/",
prompt: `Look for the "Broadway Shows" section on this page. Find the show with the absolute lowest starting price available right now and return the show name, current lowest price, and show time. Be very specific about the current price you see. Format as: Show: [name], Price: [exact current price], Time: [time]`,
});
console.log("Raw response:", response);
const result = response.data.result?.result || response.data.result || response.data;
if (result && typeof result === "string" && result.includes("Show:")) {
console.log(`🎭 Best Broadway Deal Found!`);
console.log(result);
return {
success: true,
bestDeal: result,
liveViewUrl: `https://live.anchorbrowser.io?sessionId=${session.data.id}`,
};
} else {
console.log("No Broadway deals found today");
return { success: true, message: "No deals found" };
}
} finally {
if (session?.data?.id) {
try {
await client.sessions.delete(session.data.id);
} catch (cleanupError) {
console.warn("Failed to cleanup session:", cleanupError);
}
}
}
},
});
```
### Build configuration
Since Anchor Browser uses browser automation libraries (Playwright) under the hood, we need to configure Trigger.dev to handle these dependencies properly by excluding them from the build bundle in [trigger.config.ts](https://github.com/triggerdotdev/examples/tree/main/anchor-browser-web-scraper/trigger.config.ts):
```ts
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
project: "proj_your_project_id_here", // Get from Trigger.dev dashboard
maxDuration: 3600, // 1 hour - plenty of time for web automation
dirs: ["./src/trigger"],
build: {
external: ["playwright-core", "playwright", "chromium-bidi"],
},
});
```
## Learn more
- View the [Anchor Browser docs](https://docs.anchorbrowser.io/) to learn more about Anchor Browser's AI-powered browser automation tools.
- Check out the source code for the [Anchor Browser web scraper repo](https://github.com/triggerdotdev/examples/tree/main/anchor-browser-web-scraper) on GitHub.
- Browser our [example projects](/guides/introduction) to see how you can use Trigger.dev with other services.
@@ -0,0 +1,53 @@
---
title: "Next.js Batch LLM Evaluator"
sidebarTitle: "Batch LLM Evaluator"
description: "This example Next.js project evaluates multiple LLM models using the Vercel AI SDK and streams updates to the frontend using Trigger.dev Realtime."
---
import RealtimeLearnMore from "/snippets/realtime-learn-more.mdx";
## Overview
This demo is a full stack example that uses the following:
- A [Next.js](https://nextjs.org/) app with [Prisma](https://www.prisma.io/) for the database.
- Trigger.dev [Realtime](https://trigger.dev/launchweek/0/realtime) to stream updates to the frontend.
- Work with multiple LLM models using the Vercel [AI SDK](https://sdk.vercel.ai/docs/introduction). (OpenAI, Anthropic, XAI)
- Distribute tasks across multiple tasks using the new [`batch.triggerByTaskAndWait`](https://trigger.dev/docs/triggering#batch-triggerbytaskandwait) method.
## GitHub repo
<Card
title="View the Batch LLM Evaluator repo"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/batch-llm-evaluator"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## Video
<video
controls
className="w-full aspect-video"
src="https://content.trigger.dev/batch-llm-evaluator.mp4"
></video>
## Relevant code
- View the Trigger.dev task code in the [src/trigger/batch.ts](https://github.com/triggerdotdev/examples/blob/main/batch-llm-evaluator/src/trigger/batch.ts) file.
- The `evaluateModels` task uses the `batch.triggerByTaskAndWait` method to distribute the task to the different LLM models.
- It then passes the results through to a `summarizeEvals` task that calculates some dummy "tags" for each LLM response.
- We use a [useRealtimeRunsWithTag](/realtime/react-hooks/subscribe#userealtimerunswithtag) hook to subscribe to the different evaluation tasks runs in the [src/components/llm-evaluator.tsx](https://github.com/triggerdotdev/examples/blob/main/batch-llm-evaluator/src/components/llm-evaluator.tsx) file.
- We then pass the relevant run down into three different components for the different models:
- The `AnthropicEval` component: [src/components/evals/Anthropic.tsx](https://github.com/triggerdotdev/examples/blob/main/batch-llm-evaluator/src/components/evals/Anthropic.tsx)
- The `XAIEval` component: [src/components/evals/XAI.tsx](https://github.com/triggerdotdev/examples/blob/main/batch-llm-evaluator/src/components/evals/XAI.tsx)
- The `OpenAIEval` component: [src/components/evals/OpenAI.tsx](https://github.com/triggerdotdev/examples/blob/main/batch-llm-evaluator/src/components/evals/OpenAI.tsx)
- Each of these components then uses [useRealtimeRunWithStreams](/realtime/react-hooks/streams#userealtimerunwithstreams) to subscribe to the different LLM responses.
<Note>
This example uses the older `useRealtimeRunWithStreams` hook. For new projects, consider using the new [`useRealtimeStream`](/realtime/react-hooks/streams#userealtimestream-recommended) hook (SDK 4.1.0+) for a simpler API and better type safety with defined streams.
</Note>
<RealtimeLearnMore />
@@ -0,0 +1,95 @@
---
title: "Changelog generator using Claude Agent SDK"
sidebarTitle: "Claude changelog generator"
description: "Automatically generate changelogs from your git commit history using the Claude Agent SDK and Trigger.dev."
---
## Overview
This demo how to build an AI agent using the Claude Agent SDK that explores GitHub commits, investigates unclear changes by fetching diffs on demand, and generates developer-friendly changelogs.
## Tech stack
- **[Next.js](https://nextjs.org)** Frontend framework using App Router
- **[Claude Agent SDK](https://docs.anthropic.com/en/docs/agents-and-tools/claude-agent-sdk)** Anthropic's agent SDK for building AI agents with custom tools
- **[Trigger.dev](https://trigger.dev)** workflow orchestration with real-time streaming, observability, and deployment
- **[Octokit](https://github.com/octokit/octokit.js)** GitHub API client for fetching commits and diffs
## Demo video
<video
controls
className="w-full aspect-video"
src="https://github.com/user-attachments/assets/ea151c1d-f866-4da4-90e4-96d65008d51a"
></video>
## GitHub repo
<Card
title="View the changelog generator repo"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/changelog-generator"
>
Click here to view the full open source code for this project in our examples repository on
GitHub. You can fork it and use it as a starting point for your own project.
</Card>
## How it works
The agent workflow:
1. **Receive request** User provides a GitHub repo URL and date range
2. **List commits** Agent calls `list_commits` MCP tool to get all commits
3. **Analyze commits** Agent categorizes each commit:
- Skip trivial commits (typos, formatting)
- Include clear features/improvements directly
- Investigate unclear commits by fetching their diffs
4. **Generate changelog** Agent writes a categorized markdown changelog
5. **Stream output** Changelog streams to the frontend in real-time
## Features
- **Two-phase analysis** Lists all commits first, then selectively fetches diffs only for ambiguous ones
- **Custom tools** `list_commits` and `get_commit_diff` called autonomously by Claude
- **Real-time streaming** Changelog streams to the frontend as it's generated via Trigger.dev Realtime
- **Live observability** Agent phase, turn count, and tool calls broadcast via run metadata
- **Private repo support** Optional GitHub token for private repositories
## Relevant code
| File | Description |
| ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
| [`trigger/generate-changelog.ts`](https://github.com/triggerdotdev/examples/blob/main/changelog-generator/trigger/generate-changelog.ts) | Main task with custom tools |
| [`trigger/changelog-stream.ts`](https://github.com/triggerdotdev/examples/blob/main/changelog-generator/trigger/changelog-stream.ts) | Stream definition for real-time output |
| [`app/api/generate-changelog/route.ts`](https://github.com/triggerdotdev/examples/blob/main/changelog-generator/app/api/generate-changelog/route.ts) | API endpoint that triggers the task |
| [`app/response/[runId]/page.tsx`](https://github.com/triggerdotdev/examples/blob/main/changelog-generator/app/response/%5BrunId%5D/page.tsx) | Streaming display page |
## trigger.config.ts
You need to mark the Claude Agent SDK as external in your trigger.config.ts file.
```ts trigger.config.ts
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
project: process.env.TRIGGER_PROJECT_REF!,
runtime: "node",
logLevel: "log",
maxDuration: 300,
build: {
external: ["@anthropic-ai/claude-agent-sdk"],
},
machine: "small-2x",
});
```
<Note>
Adding packages to `external` prevents them from being bundled, which is necessary for the Claude
Agent SDK. See the [build configuration docs](/config/config-file#external) for more details.
</Note>
## Learn more
- [**Building agents with Claude Agent SDK**](/guides/ai-agents/claude-code-trigger) Comprehensive guide for using Claude Agent SDK with Trigger.dev
- [**Realtime**](/realtime/overview) Stream task progress to your frontend
- [**Scheduled tasks**](/tasks/scheduled) Automate changelog generation on a schedule
@@ -0,0 +1,93 @@
---
title: "Claude GitHub wiki"
sidebarTitle: "Claude GitHub wiki"
description: "Ask questions about any public GitHub repository and get AI-powered analysis using the Claude Agent SDK and Trigger.dev."
---
## Overview
This demo shows how to build an AI agent using the Claude Agent SDK that clones any public GitHub repo and uses Claude to answer questions about its codebase. The agent explores the code using `Grep` and `Read` tools to provide detailed, accurate answers.
## Tech stack
- **[Next.js](https://nextjs.org/)** React framework with App Router for the frontend
- **[Claude Agent SDK](https://docs.anthropic.com/en/docs/agents-and-tools/claude-agent-sdk)** Anthropic's SDK for building AI agents with file system and search tools
- **[Trigger.dev](https://trigger.dev/)** workflow orchestration with real-time streaming, observability, and deployment
## Demo video
<video
controls
className="w-full aspect-video"
src="https://github.com/user-attachments/assets/ff89ae41-0488-4d1c-aa7d-4dad15cefc12"
></video>
## GitHub repo
<Card
title="View the Claude GitHub wiki agent repo"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/claude-agent-github-wiki"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## How it works
The agent workflow:
1. **Receive question** User provides a GitHub URL and question about the repo
2. **Clone repository** Shallow clone to a temp directory (depth=1 for speed)
3. **Analyze with Claude** Agent explores the codebase using allowed tools:
- `Grep` Search for patterns across files
- `Read` Read file contents
4. **Stream response** Analysis streams to the frontend in real-time
5. **Cleanup** Temp directory is always deleted, even on failure
## Features
- **Ask anything about any public repo** Architecture, security vulnerabilities, API endpoints, testing strategies, etc.
- **Claude Agent SDK exploration** Claude explores the codebase using `Grep` and `Read` tools
- **Cancel anytime** Abort long-running tasks with proper cleanup
- **Trigger.dev [Realtime](/realtime/overview) streaming** Watch Claude's analysis stream in as it's generated
- **Progress tracking** See clone status, analysis progress, and repo size via Trigger.dev metadata
## Relevant code
| File | Description |
| ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| [`trigger/analyze-repo.ts`](https://github.com/triggerdotdev/examples/blob/main/claude-agent-github-wiki/trigger/analyze-repo.ts) | Main task that clones repo, runs Claude agent, and streams response |
| [`trigger/agent-stream.ts`](https://github.com/triggerdotdev/examples/blob/main/claude-agent-github-wiki/trigger/agent-stream.ts) | Typed stream definition for real-time text responses |
| [`app/api/analyze-repo/route.ts`](https://github.com/triggerdotdev/examples/blob/main/claude-agent-github-wiki/app/api/analyze-repo/route.ts) | API endpoint that triggers the task |
| [`app/response/[runId]/page.tsx`](https://github.com/triggerdotdev/examples/blob/main/claude-agent-github-wiki/app/response/%5BrunId%5D/page.tsx) | Real-time streaming display with progress |
## trigger.config.ts
You need to mark the Claude Agent SDK as external in your trigger.config.ts file.
```ts trigger.config.ts
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
project: process.env.TRIGGER_PROJECT_REF!,
runtime: "node",
logLevel: "log",
maxDuration: 3600, // 60 minutes for large repos
build: {
external: ["@anthropic-ai/claude-agent-sdk"],
},
machine: "medium-2x",
});
```
<Note>
Adding packages to `external` prevents them from being bundled, which is necessary for the Claude
Agent SDK. See the [build configuration docs](/config/config-file#external) for more details.
</Note>
## Learn more
- [**Building agents with Claude Agent SDK**](/guides/ai-agents/claude-code-trigger) Comprehensive guide for using Claude Agent SDK with Trigger.dev
- [**Trigger.dev Realtime**](/realtime/overview) Stream task progress to your frontend
- [**Errors and retrying**](/errors-retrying) Handle failures gracefully
@@ -0,0 +1,50 @@
---
title: "Claude 3.7 thinking chatbot"
sidebarTitle: "Claude thinking chatbot"
description: "This example Next.js project uses Vercel's AI SDK and Anthropic's Claude 3.7 model to create a thinking chatbot."
---
import RealtimeLearnMore from "/snippets/realtime-learn-more.mdx";
## Overview
This demo is a full stack example that uses the following:
- A [Next.js](https://nextjs.org/) app for the chat interface
- [Trigger.dev Realtime](/realtime/overview) to stream AI responses and thinking/reasoning process to the frontend
- [Claude 3.7 Sonnet](https://www.anthropic.com/claude) for generating AI responses
- [AI SDK](https://sdk.vercel.ai/docs/introduction) for working with the Claude model
## GitHub repo
<Card
title="View the Claude thinking chatbot repo"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/claude-thinking-chatbot"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## Video
<video
controls
className="w-full aspect-video"
src="https://github.com/user-attachments/assets/053739a5-9736-4cb5-ab1c-81c35b69f4c4"
></video>
## Relevant code
- **Claude Stream Task**: View the Trigger.dev task code in the [src/trigger/claude-stream.ts](https://github.com/triggerdotdev/examples/tree/main/claude-thinking-chatbot/src/trigger/claude-stream.ts) file, which sets up the streaming connection with Claude.
- **Chat Component**: The main chat interface is in [app/components/claude-chat.tsx](https://github.com/triggerdotdev/examples/tree/main/claude-thinking-chatbot/app/components/claude-chat.tsx), which handles:
- Message state management
- User input handling
- Rendering of message bubbles
- Integration with Trigger.dev for streaming
- **Stream Response**: The `StreamResponse` component within the chat component handles:
- Displaying streaming text from Claude
- Showing/hiding the thinking process with an animated toggle
- Auto-scrolling as new content arrives
<RealtimeLearnMore />
@@ -0,0 +1,141 @@
---
title: "ClickHouse chat agent"
sidebarTitle: "ClickHouse chat agent"
description: "Build a chat agent that answers questions about your data by writing and running SQL against ClickHouse Cloud, using chat.agent() and the ClickHouse Node.js client."
---
## Overview
This example is a [chat agent](/ai-chat/overview) that answers natural-language questions about the data in a [ClickHouse Cloud](https://clickhouse.com/cloud) database. The agent discovers the schema, writes ClickHouse SQL, runs it through the official [ClickHouse Node.js client](https://clickhouse.com/docs/integrations/javascript), and streams back answers with markdown tables. Trigger.dev handles the chat session, turn loop, streaming, and resumability — the whole agent is one `chat.agent()` call and three tools.
**Tech stack:**
- **[Trigger.dev AI chat](/ai-chat/overview)** for the agent session, turn loop, and streaming
- **[ClickHouse Node.js client](https://clickhouse.com/docs/integrations/javascript)** (`@clickhouse/client`) for queries over HTTPS
- **[AI SDK](https://ai-sdk.dev/)** with Anthropic Claude for the model and tool calling
**Features:**
- **Schema discovery tools**: `listTables` reads table names, engines, and row counts from `system.tables`; `describeTable` returns column names and types using a bound `Identifier` query param, so table names are never interpolated into SQL strings
- **Read-only query tool**: `runQuery` accepts SELECT-style statements only, enforced in code and backed by ClickHouse settings — `readonly=2`, a 1,000-row result cap, and a 30 second execution timeout
- **Self-correcting SQL**: query errors are returned to the model as tool output, so the agent reads the ClickHouse error, fixes its SQL, and retries
- **Single environment variable**: the ClickHouse connection is one `CLICKHOUSE_URL` with the credentials embedded, set in the Trigger.dev dashboard
## GitHub repo
<Card
title="View the ClickHouse chat agent repo"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/clickhouse-chat-agent"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## How it works
### The agent
The agent is defined with [`chat.agent()`](/ai-chat/overview). Tools are declared on the config so tool results survive history re-conversion across turns, and the `run` function returns a `streamText()` call:
```ts trigger/clickhouse-agent.ts
import { chat } from "@trigger.dev/sdk/ai";
import { anthropic } from "@ai-sdk/anthropic";
import { stepCountIs, streamText } from "ai";
export const clickhouseAgent = chat.agent({
id: "clickhouse-agent",
idleTimeoutInSeconds: 300,
tools: { listTables, describeTable, runQuery },
run: async ({ messages, tools, signal }) => {
return streamText({
// Spread chat.toStreamTextOptions() FIRST — it wires up
// prepareStep (compaction, steering, background injection),
// the system prompt set via chat.prompt(), and telemetry.
...chat.toStreamTextOptions(),
model: anthropic("claude-opus-4-8"),
system: SYSTEM_PROMPT,
messages,
tools,
stopWhen: stepCountIs(15),
abortSignal: signal,
});
},
});
```
The system prompt tells the agent to explore the schema before querying, write ClickHouse SQL (not Postgres dialect), prefer aggregations, and present results as markdown tables.
### The query tool
`runQuery` guards against writes twice: a statement allowlist in code, and ClickHouse settings on the request itself. Errors are returned to the model instead of thrown, which is what makes the agent self-correct:
```ts trigger/clickhouse-agent.ts
const READ_ONLY_STATEMENTS = /^\s*(select|with|show|describe|desc|explain|exists)\b/i;
const runQuery = tool({
description:
"Run a read-only SQL query against ClickHouse and get the results as JSON rows.",
inputSchema: z.object({
query: z.string().describe("The ClickHouse SQL query to run"),
}),
execute: async ({ query }) => {
if (!READ_ONLY_STATEMENTS.test(query)) {
return { error: "Only read-only statements are allowed." };
}
try {
const result = await getClickHouse().query({
query,
format: "JSONEachRow",
clickhouse_settings: {
// readonly=2: reads only (no writes/DDL), but per-query settings
// like the limits below are still allowed.
readonly: "2",
max_result_rows: "1000",
result_overflow_mode: "break",
max_execution_time: 30,
},
});
const rows = await result.json();
return { rowCount: rows.length, rows };
} catch (error) {
// Return ClickHouse errors to the model so it can fix the query and retry.
return { error: error instanceof Error ? error.message : String(error) };
}
},
});
```
### Connecting to ClickHouse
The client reads a single `CLICKHOUSE_URL` environment variable — the HTTPS endpoint with credentials embedded — set in the Trigger.dev dashboard on the [Environment Variables page](/deploy-environment-variables):
```bash
CLICKHOUSE_URL=https://default:YOUR_PASSWORD@YOUR_SERVICE.clickhouse.cloud:8443
```
```ts trigger/clickhouse-agent.ts
import { createClient } from "@clickhouse/client";
const clickhouse = createClient({ url: process.env.CLICKHOUSE_URL });
```
### Chatting with the agent
Run `npx trigger.dev@latest dev`, then open the **AI agents** page in the dashboard and chat with `clickhouse-agent` in the playground. With a dataset like [NYC Taxi](https://clickhouse.com/docs/getting-started/example-datasets/nyc-taxi) loaded, asking "What were the top 5 busiest pickup days?" produces a `listTables` call, a `describeTable` call, a SQL aggregation, and a streamed markdown table of results.
## Relevant code
- **Agent + tools**: [trigger/clickhouse-agent.ts](https://github.com/triggerdotdev/examples/blob/main/clickhouse-chat-agent/trigger/clickhouse-agent.ts): the `chat.agent()` definition, the three tools, the read-only guards, and the ClickHouse client
- **Trigger config**: [trigger.config.ts](https://github.com/triggerdotdev/examples/blob/main/clickhouse-chat-agent/trigger.config.ts): project config pointing at the `trigger/` directory
## Learn more
<CardGroup cols={2}>
<Card title="AI chat overview" icon="message-bot" href="/ai-chat/overview">
How chat agents, sessions, and the turn loop work.
</Card>
<Card title="Tools" icon="wrench" href="/ai-chat/tools">
Declaring tools on your agent and how they persist across turns.
</Card>
</CardGroup>
@@ -0,0 +1,111 @@
---
title: "Background Cursor agent using the Cursor CLI"
sidebarTitle: "Cursor background agent"
description: "Run Cursor's headless CLI agent in a Trigger.dev task and stream the live output to the frontend using Trigger.dev Realtime Streams."
---
import RealtimeLearnMore from "/snippets/realtime-learn-more.mdx";
## Overview
This example runs [Cursor's headless CLI](https://cursor.com/cli) in a Trigger.dev task. The agent spawns as a child process, and its NDJSON stdout is parsed and piped to the browser in real-time using [Realtime Streams](/realtime/react-hooks/streams). The result is a live terminal UI that renders each Cursor event (system messages, assistant responses, tool calls, results) as it happens.
**Tech stack:**
- **[Next.js](https://nextjs.org/)** for the web app (App Router with server actions)
- **[Cursor CLI](https://cursor.com/cli)** for the headless AI coding agent
- **[Trigger.dev](https://trigger.dev)** for task orchestration, real-time streaming, and deployment
## Video
<video
controls
className="w-full aspect-video"
src="https://github.com/user-attachments/assets/459aa160-6659-478e-868f-32e74f79d21a"
></video>
**Features:**
- **Build extensions**: Installs the `cursor-agent` binary into the task container image using `addLayer`, demonstrating how to ship system binaries with your tasks
- **Realtime Streams v2**: NDJSON from a child process stdout is parsed and piped directly to the browser using `streams.define()` and `.pipe()`
- **Live terminal rendering**: Each Cursor event renders as a distinct row with auto-scroll
- **Long-running tasks**: Cursor agent runs for minutes; Trigger.dev handles lifecycle, timeouts, and retries automatically
- **Machine selection**: Uses the `medium-2x` preset for resource-intensive CLI tools
- **LLM model picker**: Switch between models from the UI before triggering a run
## GitHub repo
<Card
title="View the Cursor background agent repo"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/cursor-cli-demo"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## How it works
### Task orchestration
The task spawns the Cursor CLI as a child process and streams its output to the frontend:
1. A Next.js server action triggers the `cursor-agent` task with the user's prompt and selected model
2. The task spawns the Cursor CLI binary using a helper that returns a typed NDJSON stream and a `waitUntilExit()` promise
3. Each line of NDJSON stdout is parsed into typed Cursor events and piped to a Realtime Stream
4. The frontend subscribes to the stream using `useRealtimeRunWithStreams` and renders each event in a terminal UI
5. The task waits for the CLI process to exit and returns the result
### Build extension for system binaries
The example includes a custom build extension that installs `cursor-agent` into the container image using `addLayer`. The official install script is run at build time, then the resolved entry point and its dependencies are copied to a fixed path so the task can invoke them at runtime with the bundled Node binary.
```ts extensions/cursor-cli.ts
const CURSOR_AGENT_DIR = "/usr/local/lib/cursor-agent";
export const cursorCli = (): BuildExtension => ({
name: "cursor-cli",
onBuildComplete(context) {
if (context.target === "dev") return;
context.addLayer({
id: "cursor-cli",
image: {
instructions: [
"RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/*",
'ENV PATH="/root/.local/bin:$PATH"',
"RUN curl -fsSL https://cursor.com/install | bash",
`RUN cp -r $(dirname $(readlink -f /root/.local/bin/cursor-agent)) ${CURSOR_AGENT_DIR}`,
],
},
});
},
});
```
### Streaming with Realtime Streams v2
The stream is defined with a typed schema and piped from the child process:
```ts trigger/cursor-stream.ts
export const cursorStream = streams.define("cursor", cursorEventSchema);
```
```ts trigger/cursor-agent.ts
const { stream, waitUntilExit } = spawnCursorAgent({ prompt, model });
cursorStream.pipe(stream);
await waitUntilExit();
```
On the frontend, the `useRealtimeRunWithStreams` hook subscribes to these events and renders them as they arrive.
## Relevant code
- **Build extension + spawn helper**: [extensions/cursor-cli.ts](https://github.com/triggerdotdev/examples/blob/main/cursor-cli-demo/extensions/cursor-cli.ts): installs the binary and provides a typed NDJSON stream with `waitUntilExit()`
- **Task definition**: [trigger/cursor-agent.ts](https://github.com/triggerdotdev/examples/blob/main/cursor-cli-demo/trigger/cursor-agent.ts): spawns the CLI, pipes the stream, waits for exit
- **Stream definition**: [trigger/cursor-stream.ts](https://github.com/triggerdotdev/examples/blob/main/cursor-cli-demo/trigger/cursor-stream.ts): Realtime Streams v2 stream with typed schema
- **Terminal UI**: [components/terminal.tsx](https://github.com/triggerdotdev/examples/blob/main/cursor-cli-demo/components/terminal.tsx): renders live events using `useRealtimeRunWithStreams`
- **Event types**: [lib/cursor-events.ts](https://github.com/triggerdotdev/examples/blob/main/cursor-cli-demo/lib/cursor-events.ts): TypeScript types and parsers for Cursor NDJSON events
- **Trigger config**: [trigger.config.ts](https://github.com/triggerdotdev/examples/blob/main/cursor-cli-demo/trigger.config.ts): project config with the cursor CLI build extension
<RealtimeLearnMore />
@@ -0,0 +1,86 @@
---
title: "Human-in-the-loop workflow with ReactFlow and Trigger.dev waitpoint tokens"
sidebarTitle: "Human-in-the-loop workflow"
description: "This example project creates audio summaries of newspaper articles using a human-in-the-loop workflow built with ReactFlow and Trigger.dev waitpoint tokens."
---
## Overview
This demo is a full stack example that uses the following:
- [Next.js](https://nextjs.org/) for the web application
- [ReactFlow](https://reactflow.dev/) for the workflow UI
- [Trigger.dev Realtime](/realtime/overview) to subscribe to task runs and show the real-time status of the workflow steps
- [Trigger.dev waitpoint tokens](/wait-for-token) to create a human-in-the-loop flow with a review step
- [OpenAI API](https://openai.com/api/) to generate article summaries
- [ElevenLabs](https://elevenlabs.io/text-to-speech) to convert text to speech
## GitHub repo
<Card
title="View the human-in-the-loop workflow repo"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/article-summary-workflow"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## Video
<video
controls
className="w-full aspect-video"
src="https://content.trigger.dev/reactflow-waitpoints-example.mov"
></video>
## Relevant code
Each node in the workflow corresponds to a Trigger.dev task. The idea is to enable building flows by composition of different tasks. The output of one task serves as input for another.
- **Trigger.dev task splitting**:
- The [summarizeArticle](https://github.com/triggerdotdev/examples/blob/main/article-summary-workflow/src/trigger/summarizeArticle.ts) task uses the OpenAI API to generate a summary an article.
- The [convertTextToSpeech](https://github.com/triggerdotdev/examples/blob/main/article-summary-workflow/src/trigger/convertTextToSpeech.ts) task uses the ElevenLabs API to convert the summary into an audio stream and upload it to an S3 bucket.
- The [reviewSummary](https://github.com/triggerdotdev/examples/blob/main/article-summary-workflow/src/trigger/reviewSummary.ts) task is a human-in-the-loop step that shows the result and waits for approval of the summary before continuing.
- [articleWorkflow](https://github.com/triggerdotdev/examples/blob/main/article-summary-workflow/src/trigger/articleWorkflow.ts) is the entrypoint that ties the workflow together and orchestrates the tasks. You might choose to approach the orchestration differently, depending on your use case.
- **ReactFlow Nodes**: there are three types of nodes in this example. All of them are custom ReactFlow nodes.
- The [InputNode](https://github.com/triggerdotdev/examples/blob/main/article-summary-workflow/src/components/InputNode.tsx) is the starting node of the workflow. It triggers the workflow by submitting an article URL.
- The [ActionNode](https://github.com/triggerdotdev/examples/blob/main/article-summary-workflow/src/components/ActionNode.tsx) is a node that shows the status of a task run in Trigger.dev, in real-time using the React hooks for Trigger.dev.
- The [ReviewNode](https://github.com/triggerdotdev/examples/blob/main/article-summary-workflow/src/components/ReviewNode.tsx) is a node that shows the summary result and prompts the user for approval before continuing. It uses the Realtime API to fetch details about the review status. Also, it interacts with the Trigger.dev waitpoint API for completing the waitpoint token using Next.js server actions.
- **Workflow orchestration**:
- The workflow is orchestrated by the [Flow](https://github.com/triggerdotdev/examples/blob/main/article-summary-workflow/src/components/Flow.tsx) component. It lays out the nodes, the connections between them, as well as the mapping to the Trigger.dev tasks.
It also uses the `useRealtimeRunsWithTag` hook to subscribe to task runs associated with the workflow and passes down the run details to the nodes.
The waitpoint token is created in [a Next.js server action](https://github.com/triggerdotdev/examples/blob/main/article-summary-workflow/src/app/actions.ts#L26):
```ts
const reviewWaitpointToken = await wait.createToken({
tags: [workflowTag],
timeout: "1h",
idempotencyKey: `review-summary-${workflowTag}`,
});
```
and later completed in another server action in the same file:
```ts
await wait.completeToken<ReviewPayload>(
{ id: tokenId },
{
approved: true,
approvedAt: new Date(),
approvedBy: user,
}
);
```
While the workflow in this example is static and does not allow changing the connections between nodes in the UI, it serves as a good baseline for understanding how to build completely custom workflow builders using Trigger.dev and ReactFlow.
## Learn more about Trigger.dev Realtime and waitpoint tokens
To learn more, take a look at the following resources:
- [Trigger.dev Realtime](/realtime) - learn more about how to subscribe to runs and get real-time updates
- [Realtime streaming](/realtime/react-hooks/streams) - learn more about streaming data from your tasks
- [React hooks](/realtime/react-hooks) - learn more about using React hooks to interact with the Trigger.dev API
- [Waitpoint tokens](/wait-for-token) - learn about waitpoint tokens in Trigger.dev and human-in-the-loop flows
@@ -0,0 +1,92 @@
---
title: "Mastra agents with memory sharing + Trigger.dev task orchestration"
sidebarTitle: "Mastra agents with memory"
description: "Multi-agent workflow with persistent memory sharing using Mastra and Trigger.dev for clothing recommendations based on weather data."
---
## Overview
Enter a city and an activity, and get a clothing recommendation generated for you based on today's weather.
![Generated clothing recommendations](https://github.com/user-attachments/assets/edfca304-6b22-4fa8-9362-71ecb3fe4903)
By combining Mastra's persistent memory system and agent orchestration with Trigger.dev's durable task execution, retries and observability, you get production-ready AI workflows that survive failures, scale automatically, and maintain context across long-running operations.
## Tech stack
- **[Node.js](https://nodejs.org)** runtime environment
- **[Mastra](https://mastra.ai)** for AI agent orchestration and memory management (Mastra is a Typescript framework for building AI agents, and uses Vercel's AI Agent SDK under the hood.)
- **[PostgreSQL](https://postgresql.org)** for persistent storage and memory sharing
- **[Trigger.dev](https://trigger.dev)** for task orchestration, batching, and observability
- **[OpenAI GPT-4](https://openai.com)** for natural language processing
- **[Open-Meteo API](https://open-meteo.com)** for weather data (no API key required)
- **[Zod](https://zod.dev)** for schema validation and type safety
## GitHub repo
<Card
title="View the Mastra agents with memory repo"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/mastra-agents"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## Featured patterns
- **[Agent Memory Sharing](https://github.com/triggerdotdev/examples/blob/main/mastra-agents/src/trigger/weather-task.ts)**: Efficient data sharing between agents using Mastra's working memory system
- **[Task Orchestration](https://github.com/triggerdotdev/examples/blob/main/mastra-agents/src/trigger/weather-task.ts)**: Multi-step workflows with `triggerAndWait` for sequential agent execution
- **[Centralized Storage](https://github.com/triggerdotdev/examples/blob/main/mastra-agents/src/mastra/index.ts)**: Single PostgreSQL storage instance shared across all agents to prevent connection duplication
- **[Custom Tools](https://github.com/triggerdotdev/examples/blob/main/mastra-agents/src/mastra/tools/weather-tool.ts)**: External API integration with structured output validation
- **[Agent Specialization](https://github.com/triggerdotdev/examples/blob/main/mastra-agents/src/mastra/agents/)**: Purpose-built agents with specific roles and instructions
- **[Schema Optimization](https://github.com/triggerdotdev/examples/blob/main/mastra-agents/src/mastra/schemas/weather-data.ts)**: Lightweight data structures for performance
## Project Structure
```
src/
├── mastra/
│ ├── agents/
│ │ ├── weather-analyst.ts # Weather data collection
│ │ ├── clothing-advisor.ts # Clothing recommendations
│ ├── tools/
│ │ └── weather-tool.ts # Enhanced weather API tool
│ ├── schemas/
│ │ └── weather-data.ts # Weather schema
│ └── index.ts # Mastra configuration
├── trigger/
│ └── weather-task.ts # Trigger.dev tasks
```
## Relevant code
- **[Multi-step task orchestration](https://github.com/triggerdotdev/examples/blob/main/mastra-agents/src/trigger/weather-task.ts)**: Multi-step task orchestration with `triggerAndWait` for sequential agent execution and shared memory context
- **[Weather analyst agent](https://github.com/triggerdotdev/examples/blob/main/mastra-agents/src/mastra/agents/weather-analyst.ts)**: Specialized agent for weather data collection with external API integration and memory storage
- **[Clothing advisor agent](https://github.com/triggerdotdev/examples/blob/main/mastra-agents/src/mastra/agents/clothing-advisor.ts)**: Purpose-built agent that reads from working memory and generates natural language responses
- **[Weather tool](https://github.com/triggerdotdev/examples/blob/main/mastra-agents/src/mastra/tools/weather-tool.ts)**: Custom Mastra tool with Zod validation for external API calls and error handling
- **[Weather data schema](https://github.com/triggerdotdev/examples/blob/main/mastra-agents/src/mastra/schemas/weather-data.ts)**: Optimized Zod schema for efficient memory storage and type safety
- **[Mastra configuration](https://github.com/triggerdotdev/examples/blob/main/mastra-agents/src/mastra/index.ts)**: Mastra configuration with PostgreSQL storage and agent registration
## Storage Architecture
This project uses a **centralized PostgreSQL storage** approach where a single database connection is shared across all Mastra agents. This prevents duplicate database connections and ensures efficient memory sharing between the weather analyst and clothing advisor agents.
### Storage Configuration
The storage is configured once in the main Mastra instance (`src/mastra/index.ts`) and automatically inherited by all agent Memory instances. This eliminates the "duplicate database object" warning that can occur with multiple PostgreSQL connections.
The PostgreSQL storage works seamlessly in both local development and serverless environments with any PostgreSQL provider, such as:
- [Local PostgreSQL instance](https://postgresql.org)
- [Supabase](https://supabase.com) - Serverless PostgreSQL
- [Neon](https://neon.tech) - Serverless PostgreSQL
- [Railway](https://railway.app) - Simple PostgreSQL hosting
- [AWS RDS](https://aws.amazon.com/rds/postgresql/) - Managed PostgreSQL
## Learn More
To learn more about the technologies used in this project, check out the following resources:
- [Mastra docs](https://mastra.ai/en/docs) - learn about AI agent orchestration and memory management
- [Mastra working memory](https://mastra.ai/en/docs/memory/overview) - learn about efficient data sharing between agents
@@ -0,0 +1,55 @@
---
title: "Meme generator with human-in-the-loop approval"
sidebarTitle: "AI meme generator"
description: "This example project creates memes using OpenAI's DALL-E 3 with a human-in-the-loop approval workflow built using Trigger.dev waitpoint tokens."
---
## Overview
This demo is a full stack example that uses the following:
- A [Next.js](https://nextjs.org/) app, with an [endpoint](https://github.com/triggerdotdev/examples/blob/main/meme-generator-human-in-the-loop/src/app/endpoints/[slug]/page.tsx) for approving the generated memes
- [Trigger.dev](https://trigger.dev) tasks to generate the images and orchestrate the waitpoint workflow
- [OpenAI DALL-E 3](https://platform.openai.com/docs/guides/images) for generating the images
- A [Slack app](https://api.slack.com/quickstart) for the human-in-the-loop step, with the approval buttons linked to the endpoint
## GitHub repo
<Card
title="View the meme generator human-in-the-loop example repo"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/meme-generator-human-in-the-loop"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## Post to Slack
![Meme Generator with Human-in-the-Loop Approval](/images/slack-meme-approval.png)
## Relevant code
- **Meme generator task**:
- The [memegenerator.ts](https://github.com/triggerdotdev/examples/blob/main/meme-generator-human-in-the-loop/src/trigger/memegenerator.ts) task:
- Generates two meme variants using DALL-E 3
- Uses [batchTriggerAndWait](/triggering#yourtask-batchtriggerandwait) to generate multiple meme variants simultaneously (this is because you can only generate 1 image at a time with DALL-E 3)
- Creates a [waitpoint token](/wait-for-token)
- Sends the generated images with approval buttons to Slack for review
- Handles the approval workflow
- **Approval Endpoint**:
- The waitpoint approval handling is in [page.tsx](https://github.com/triggerdotdev/examples/blob/main/meme-generator-human-in-the-loop/src/app/endpoints/[slug]/page.tsx), which processes:
- User selections from Slack buttons
- Waitpoint completion with the chosen meme variant
- Success/failure feedback to the approver
## Learn more
To learn more, take a look at the following resources:
- [Waitpoint tokens](/wait-for-token) - learn about waitpoint tokens in Trigger.dev and human-in-the-loop flows
- [OpenAI DALL-E API](https://platform.openai.com/docs/guides/images) - learn about the DALL-E image generation API
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API
- [Slack Incoming Webhooks](https://api.slack.com/messaging/webhooks) - learn about integrating with Slack
@@ -0,0 +1,60 @@
---
title: "OpenAI Agents SDK for Python guardrails"
sidebarTitle: "OpenAI Agents SDK for Python guardrails"
description: "This example project demonstrates how to implement different types of guardrails using the OpenAI Agent SDK for Python with Trigger.dev."
---
## Overview
This demo is a practical guide that demonstrates:
- **Three types of AI guardrails**: Input validation, output checking, and real-time streaming monitoring
- Integration of the [OpenAI Agent SDK for Python](https://openai.github.io/openai-agents-python/) with [Trigger.dev](https://trigger.dev) for production AI workflows
- Triggering Python scripts from tasks using our [Python build extension](/config/extensions/pythonExtension)
- **Educational examples** of implementing guardrails for AI safety and control mechanisms
- Real-world scenarios like math tutoring agents with content validation and complexity monitoring
Guardrails are safety mechanisms that run alongside AI agents to validate input, check output, monitor streaming content in real-time, and prevent unwanted or harmful behavior.
## GitHub repo
<Card
title="View the OpenAI Agent SDK Guardrails repo"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/openai-agent-sdk-guardrails-examples"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## Video
<video
controls
className="w-full aspect-video"
src="https://github.com/user-attachments/assets/9b1e55c7-467d-4aca-8b4a-a018014c0827"
></video>
## Relevant code
### Trigger.dev Tasks
- **[inputGuardrails.ts](https://github.com/triggerdotdev/examples/blob/main/openai-agent-sdk-guardrails-examples/src/trigger/inputGuardrails.ts)** - Passes user prompts to Python script and handles `InputGuardrailTripwireTriggered` exceptions
- **[outputGuardrails.ts](https://github.com/triggerdotdev/examples/blob/main/openai-agent-sdk-guardrails-examples/src/trigger/outputGuardrails.ts)** - Runs agent generation and catches `OutputGuardrailTripwireTriggered` exceptions with detailed error info
- **[streamingGuardrails.ts](https://github.com/triggerdotdev/examples/blob/main/openai-agent-sdk-guardrails-examples/src/trigger/streamingGuardrails.ts)** - Executes streaming Python script and parses JSON output containing guardrail metrics
### Python Implementations
- **[input-guardrails.py](https://github.com/triggerdotdev/examples/blob/main/openai-agent-sdk-guardrails-examples/src/python/input-guardrails.py)** - Agent with `@input_guardrail` decorator that validates user input before processing (example: math tutor that only responds to math questions)
- **[output-guardrails.py](https://github.com/triggerdotdev/examples/blob/main/openai-agent-sdk-guardrails-examples/src/python/output-guardrails.py)** - Agent with `@output_guardrail` decorator that validates generated responses using a separate guardrail agent
- **[streaming-guardrails.py](https://github.com/triggerdotdev/examples/blob/main/openai-agent-sdk-guardrails-examples/src/python/streaming-guardrails.py)** - Processes `ResponseTextDeltaEvent` streams with async guardrail checks at configurable intervals (example: stops streaming if language is too complex for a 10-year-old)
### Configuration
- **[trigger.config.ts](https://github.com/triggerdotdev/examples/blob/main/openai-agent-sdk-guardrails-examples/trigger.config.ts)** - Uses the Trigger.dev Python extension
### Learn more
- [OpenAI Agent SDK documentation](https://openai.github.io/openai-agents-python/)
- [OpenAI Agent SDK guardrails](https://openai.github.io/openai-agents-python/guardrails/)
- Our [Python build extension](/config/extensions/pythonExtension#python)
@@ -0,0 +1,64 @@
---
title: "OpenAI Agents SDK for Typescript + Trigger.dev playground"
sidebarTitle: "OpenAI Agents SDK for Typescript playground"
description: "Build production-ready AI agents with OpenAI Agents SDK for Typescript and Trigger.dev. Explore 7 examples covering streaming, multi-agent systems, and tool integration."
---
## Overview
7 production-ready patterns built with the OpenAI Agents SDK and Trigger.dev. Clone this repo to experiment with everything from basic calls to workflows with tools, streaming, guardrails, handoffs, and more.
By combining the OpenAI Agents SDK with Trigger.dev, you can create durable agents that can be deployed to production and scaled to any size, with retries, queues, and full observability built-in.
## Video
<video
controls
className="w-full aspect-video"
src="https://github.com/user-attachments/assets/d3a1c709-412f-48e8-a4aa-f0ef50dce5c8"
></video>
## Tech stack
- [Node.js](https://nodejs.org) runtime environment
- [OpenAI Agents SDK for Typescript](https://openai.github.io/openai-agents-js/) for creating and managing AI agents
- [Trigger.dev](https://trigger.dev) for task orchestration, batching, scheduling, and workflow management
- [Zod](https://zod.dev) for payload validation
## GitHub repo
<Card
title="View the OpenAI Agents SDK TypeScript playground repo"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/openai-agents-sdk-with-trigger-playground"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## Agent tasks
- **Basic Agent Chat**: Personality-based conversations with strategic model selection
- **Agent with Tools**: A simple agent that can call tools to get weather data
- **Streaming Agent**: Real-time content generation with progress tracking
- **Agent Handoffs**: True multi-agent collaboration using the [handoff pattern](https://openai.github.io/openai-agents-js/guides/handoffs/) where agents can dynamically transfer control to specialists
- **Parallel Agents**: Concurrent agent processing for complex analysis tasks
- **Scheduled Agent**: Time-based agent workflows for continuous monitoring
- **Agent with Guardrails**: Input guardrails for safe AI interactions
## Relevant code
- **[basicAgentChat.ts](https://github.com/triggerdotdev/examples/blob/main/openai-agents-sdk-with-trigger-playground/src/trigger/basicAgentChat.ts)** - Strategic model selection (GPT-4, o1-preview, o1-mini, gpt-4o-mini) mapped to personality types with Trigger.dev task orchestration
- **[agentWithTools.ts](https://github.com/triggerdotdev/examples/blob/main/openai-agents-sdk-with-trigger-playground/src/trigger/agentWithTools.ts)** - OpenAI tool calling with Zod validation integrated into Trigger.dev's retry and error handling mechanisms
- **[streamingAgent.ts](https://github.com/triggerdotdev/examples/blob/main/openai-agents-sdk-with-trigger-playground/src/trigger/streamingAgent.ts)** - Native OpenAI streaming responses with real-time progress tracking via Trigger.dev metadata
- **[scheduledAgent.ts](https://github.com/triggerdotdev/examples/blob/main/openai-agents-sdk-with-trigger-playground/src/trigger/scheduledAgent.ts)** - Cron-scheduled OpenAI agents running every 6 hours with automatic trend analysis
- **[parallelAgents.ts](https://github.com/triggerdotdev/examples/blob/main/openai-agents-sdk-with-trigger-playground/src/trigger/parallelAgents.ts)** - Concurrent OpenAI agent execution using Trigger.dev batch operations (`batch.triggerByTaskAndWait`) for scalable text analysis
- **[agentWithGuardrails.ts](https://github.com/triggerdotdev/examples/blob/main/openai-agents-sdk-with-trigger-playground/src/trigger/agentWithGuardrails.ts)** - OpenAI classification agents as input guardrails with structured validation and exception handling
- **[agentHandoff.ts](https://github.com/triggerdotdev/examples/blob/main/openai-agents-sdk-with-trigger-playground/src/trigger/agentHandoff.ts)** - OpenAI Agents SDK handoff pattern with specialist delegation orchestrated through Trigger.dev workflows
## Learn more
- [OpenAI Agents SDK docs](https://openai.github.io/openai-agents-js/) - learn about creating and managing AI agents
- [OpenAI Agents SDK handoffs](https://openai.github.io/openai-agents-js/guides/handoffs/) - learn about agent-to-agent delegation patterns
- [Batch triggering](/triggering#batch-trigger) - learn about parallel task execution
- [Scheduled tasks (cron)](/tasks/scheduled#scheduled-tasks-cron) - learn about cron-based task scheduling
@@ -0,0 +1,61 @@
---
title: "Product image generator using Replicate and Trigger.dev"
sidebarTitle: "Product image generator"
description: "AI-powered product image generator that transforms basic product photos into professional marketing shots using Replicate's image generation models"
---
## Overview
This project demonstrates how to build an AI-powered product image generator that transforms basic product photos into professional marketing shots. Users upload a product image and receive three professionally styled variations: clean product shots, lifestyle scenes, and hero shots with dramatic lighting.
## Video
<video
controls
className="w-full aspect-video"
src="https://content.trigger.dev/product-image-generator-example.mp4"
/>
## GitHub repo
Clone this repo and follow the instructions in the `README.md` file to get started.
<Card
title="View the product image generator repo"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/product-image-generator"
>
Click here to view the full code in our examples repository on GitHub. You can fork it and use it
as a starting point for your project.
</Card>
## Tech stack
- [**Next.js**](https://nextjs.org/) frontend React framework
- [**Replicate**](https://replicate.com/docs) AI image generation using the `google/nano-banana` image-to-image model
- [**UploadThing**](https://uploadthing.com/) file upload management and server callbacks
- [**Cloudflare R2**](https://developers.cloudflare.com/r2/) scalable image storage with public URLs
## How it works
The application orchestrates image generation through two main tasks: [`generateImages`](https://github.com/triggerdotdev/examples/blob/main/product-image-generator/app/trigger/generate-images.ts) coordinates batch processing, while [`generateImage`](https://github.com/triggerdotdev/examples/blob/main/product-image-generator/app/trigger/generate-images.ts) handles individual style generation.
Each generation task enhances prompts with style-specific instructions, calls Replicate's `google/nano-banana` image-to-image model, creates waitpoint tokens for async webhook handling, and uploads results to Cloudflare R2. The frontend displays real-time progress updates via React hooks as tasks complete.
Style presets include clean product shots (white background), lifestyle scenes (person holding product), and hero shots (dramatic lighting).
## Relevant code
- **Image generation tasks** batch processing with waitpoints for Replicate webhook callbacks ([`app/trigger/generate-images.ts`](https://github.com/triggerdotdev/examples/blob/main/product-image-generator/app/trigger/generate-images.ts))
- **Upload handler** UploadThing integration that triggers batch generation ([`app/api/uploadthing/core.ts`](https://github.com/triggerdotdev/examples/blob/main/product-image-generator/app/api/uploadthing/core.ts))
- **Real-time progress UI** live task updates using React hooks ([`app/components/GeneratedCard.tsx`](https://github.com/triggerdotdev/examples/blob/main/product-image-generator/app/components/GeneratedCard.tsx))
- **Custom prompt interface** user-defined style generation ([`app/components/CustomPromptCard.tsx`](https://github.com/triggerdotdev/examples/blob/main/product-image-generator/app/components/CustomPromptCard.tsx))
- **Main app component** layout and state management ([`app/ProductImageGenerator.tsx`](https://github.com/triggerdotdev/examples/blob/main/product-image-generator/app/ProductImageGenerator.tsx))
## Learn more
- [**Waitpoints**](/wait-for-token) pause tasks for async webhook callbacks
- [**React hooks**](/realtime/react-hooks/overview) real-time task updates and frontend integration
- [**Batch operations**](/triggering#tasks-batchtrigger) parallel task execution patterns
- [**Replicate API**](https://replicate.com/docs/get-started/nextjs) AI model integration
- [**UploadThing**](https://docs.uploadthing.com/) file upload handling and server callbacks
@@ -0,0 +1,44 @@
---
title: "Next.js Realtime CSV Importer"
sidebarTitle: "Realtime CSV Importer"
description: "This example Next.js project demonstrates how to use Trigger.dev Realtime to build a CSV Uploader with progress updates streamed to the frontend."
---
import RealtimeLearnMore from "/snippets/realtime-learn-more.mdx";
## Overview
The frontend is a Next.js app that allows users to upload a CSV file, which is then processed in the background using Trigger.dev tasks. The progress of the task is streamed back to the frontend in real-time using Trigger.dev Realtime.
- A [Next.js](https://nextjs.org/) app with [Trigger.dev](https://trigger.dev/) for the background tasks.
- [UploadThing](https://uploadthing.com/) to handle CSV file uploads
- Trigger.dev [Realtime](https://trigger.dev/launchweek/0/realtime) to stream updates to the frontend.
## GitHub repo
<Card
title="View the Realtime CSV Importer repo"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/blob/main/realtime-csv-importer/README.md"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## Video
<video
controls
className="w-full aspect-video"
src="https://github.com/user-attachments/assets/25160343-6663-452c-8a27-819c3fd7b8df"
></video>
## Relevant code
- View the Trigger.dev task code in the [src/trigger/csv.ts](https://github.com/triggerdotdev/examples/blob/main/realtime-csv-importer/src/trigger/csv.ts) file.
- The parent task `csvValidator` downloads the CSV file, parses it, and then splits the rows into multiple batches. It then does a `batch.triggerAndWait` to distribute the work the `handleCSVRow` task.
- The `handleCSVRow` task "simulates" checking the row for a valid email address and then updates the progress of the parent task using `metadata.parent`. See the [Trigger.dev docs](/runs/metadata#parent-and-root-updates) for more information on how to use the `metadata.parent` object.
- The `useRealtimeCSVValidator` hook in the [src/hooks/useRealtimeCSVValidator.ts](https://github.com/triggerdotdev/examples/blob/main/realtime-csv-importer/src/hooks/useRealtimeCSVValidator.ts) file handles the call to `useRealtimeRun` to get the progress of the parent task.
- The `CSVProcessor` component in the [src/components/CSVProcessor.tsx](https://github.com/triggerdotdev/examples/blob/main/realtime-csv-importer/src/components/CSVProcessor.tsx) file handles the file upload and displays the progress bar, and uses the `useRealtimeCSVValidator` hook to get the progress updates.
<RealtimeLearnMore />
@@ -0,0 +1,43 @@
---
title: "Image generation with Fal.ai and Trigger.dev Realtime"
sidebarTitle: "Realtime image gen with Fal.ai"
description: "This example Next.js project generates an image from a prompt using Fal.ai and shows the progress of the task on the frontend using Trigger.dev Realtime."
---
import RealtimeLearnMore from "/snippets/realtime-learn-more.mdx";
## Overview
This full stack Next.js project showcases the following:
- A Trigger.dev task which [generates an image from a prompt using Fal.ai](https://github.com/triggerdotdev/examples/blob/main/realtime-fal-ai-image-generation/src/trigger/realtime-generate-image.ts)
- When a [form is submitted](https://github.com/triggerdotdev/examples/blob/main/realtime-fal-ai-image-generation/src/app/page.tsx) in the UI, triggering the task using a [server action](https://github.com/triggerdotdev/examples/blob/main/realtime-fal-ai-image-generation/src/app/actions/process-image.ts)
- Showing the [progress of the task](https://github.com/triggerdotdev/examples/blob/main/realtime-fal-ai-image-generation/src/app/processing/%5Bid%5D/ProcessingContent.tsx) on the frontend using Trigger.dev Realtime. This also includes error handling and a fallback UI
- Once the task is completed, showing the generated image on the frontend next to the original image
## GitHub repo
<Card
title="View the project on GitHub"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/realtime-fal-ai-image-generation"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## Walkthrough video
This video walks through the process of creating this task in a Next.js project.
<iframe
width="100%"
height="315"
src="https://www.youtube.com/embed/BWZqYfUaigg?si=XpqVUEIf1j4bsYZ4"
title="Trigger.dev walkthrough"
allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerPolicy="strict-origin-when-cross-origin"
allowFullScreen
/>
<RealtimeLearnMore />
@@ -0,0 +1,92 @@
---
title: "Smart Spreadsheet"
sidebarTitle: "Smart Spreadsheet"
description: "An AI-powered company enrichment tool that uses Exa search and Claude to extract verified company data with source attribution."
---
import RealtimeLearnMore from "/snippets/realtime-learn-more.mdx";
## Overview
Smart Spreadsheet is an AI-powered tool that enriches company data on demand. Input a company name or website URL and get verified information including industry, headcount, and funding details; each with source attribution. Results appear in the frontend in real-time as each task completes.
- A [Next.js](https://nextjs.org/) app with [Trigger.dev](https://trigger.dev/) for background tasks
- [Exa](https://exa.ai/) an AI-native search engine that returns clean, structured content ready for LLM extraction
- [Claude](https://anthropic.com/) via the [Vercel AI SDK](https://sdk.vercel.ai/) for data extraction
- [Supabase](https://supabase.com/) PostgreSQL database for persistence
- Trigger.dev [Realtime](/realtime/overview) for live updates to the frontend
## Video
<video
controls
className="w-full aspect-video"
src="https://content.trigger.dev/smart-spreadsheet.mp4"
></video>
## GitHub repo
<Card
title="View the Smart Spreadsheet repo"
icon="github"
href="https://github.com/triggerdotdev/examples/tree/main/smart-spreadsheet"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## How it works
The enrichment workflow:
1. **Trigger enrichment** User enters a company name or URL in the spreadsheet UI
2. **Parallel data gathering** Four subtasks run concurrently to fetch basic info, industry, employee count, and funding details
3. **AI extraction** Each subtask uses Exa search + Claude to extract structured data with source URLs
4. **Real-time updates** Results appear in the frontend as each subtask completes
5. **Persist results** Enriched data is saved to Supabase with source attribution
## Features
- **Parallel processing** All four enrichment categories run simultaneously using [batch.triggerByTaskAndWait](/triggering#batch-trigger-by-task-and-wait)
- **Source attribution** Every data point includes the URL it was extracted from
- **Live updates** Results appear in the UI as each task completes using [Realtime](/realtime/overview)
- **Structured extraction** Zod schemas ensure consistent data output from Claude
## Key code patterns
### Parallel task execution
The main task triggers all four enrichment subtasks simultaneously using `batch.triggerByTaskAndWait`:
```ts src/trigger/enrich-company.ts
const { runs } = await batch.triggerByTaskAndWait([
{ task: getBasicInfo, payload: { companyName, companyUrl } },
{ task: getIndustry, payload: { companyName, companyUrl } },
{ task: getEmployeeCount, payload: { companyName, companyUrl } },
{ task: getFundingRound, payload: { companyName, companyUrl } },
]);
```
### Live updates from child tasks
Each subtask uses `metadata.parent.set()` to update the parent's metadata as soon as data is extracted:
```ts src/trigger/get-basic-info.ts
// After Claude extracts the data, update the parent task's metadata
metadata.parent.set("website", object.website);
metadata.parent.set("description", object.description);
```
The frontend subscribes to these metadata updates using [Realtime](/realtime/overview), so users see each field populate as it's discovered.
## Relevant code
| File | Description |
| ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| [`src/trigger/enrich-company.ts`](https://github.com/triggerdotdev/examples/blob/main/smart-spreadsheet/src/trigger/enrich-company.ts) | Main orchestrator that triggers parallel subtasks and persists results |
| [`src/trigger/get-basic-info.ts`](https://github.com/triggerdotdev/examples/blob/main/smart-spreadsheet/src/trigger/get-basic-info.ts) | Extracts company website and description |
| [`src/trigger/get-industry.ts`](https://github.com/triggerdotdev/examples/blob/main/smart-spreadsheet/src/trigger/get-industry.ts) | Classifies company industry |
| [`src/trigger/get-employee-count.ts`](https://github.com/triggerdotdev/examples/blob/main/smart-spreadsheet/src/trigger/get-employee-count.ts) | Finds employee headcount |
| [`src/trigger/get-funding-round.ts`](https://github.com/triggerdotdev/examples/blob/main/smart-spreadsheet/src/trigger/get-funding-round.ts) | Discovers latest funding information |
<RealtimeLearnMore />
@@ -0,0 +1,195 @@
---
title: "Turborepo monorepo with Prisma"
sidebarTitle: "Turborepo monorepo with Prisma"
description: "Two example projects demonstrating how to use Prisma and Trigger.dev in a Turborepo monorepo setup."
---
## Overview
These examples demonstrate two different ways of using Prisma and Trigger.dev in a Turborepo monorepo. In both examples, a task is triggered from a Next.js app using a server action, which uses Prisma to add a user to a database table. The examples differ in how Trigger.dev is installed and configured.
- Example 1: Turborepo monorepo demo with Trigger.dev and Prisma packages
- Example 2: Turborepo monorepo demo with a Prisma package and Trigger.dev installed in a Next.js app
<Note>
You can either fork the repos below, or simply check out the project structures and code to get an idea of how to set up Trigger.dev in your own monorepos.
</Note>
## Example 1: Turborepo monorepo demo with Trigger.dev and Prisma packages
This simple example demonstrates how to use Trigger.dev and Prisma as packages inside a monorepo created with Turborepo. The Trigger.dev task is triggered by a button click in a Next.js app which triggers the task via a server action.
### GitHub repo
Fork the GitHub repo below to get started with this example project.
<Card
title="Check out the Turborepo monorepo demo with Trigger.dev and Prisma packages"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-package"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
### Features
- This monorepo has been created using the [Turborepo CLI](https://turbo.build/repo), following the official [Prisma and Turborepo docs](https://www.prisma.io/docs/guides/turborepo), and then adapted for use with Trigger.dev.
- [pnpm](https://pnpm.io/) has been used as the package manager.
- A tasks package (`@repo/tasks`) using [Trigger.dev](https://trigger.dev) is used to create and execute tasks from an app inside the monorepo.
- A database package (`@repo/db`) using [Prisma ORM](https://www.prisma.io/docs/orm/) is used to interact with the database. You can use any popular Postgres database supported by Prisma, e.g. [Supabase](https://supabase.com/), [Neon](https://neon.tech/), etc.
- A [Next.js](https://nextjs.org/) example app (`apps/web`) to show how to trigger the task via a server action.
### Project structure
Simplified project structure for this example:
```
|
| — apps/
| | — web/ # Next.js frontend application
| | | — app/ # Next.js app router
| | | | — api/
| | | | | — actions.ts # Server actions for triggering tasks
| | | | — page.tsx # Main page with "Add new user" button
| | | | — layout.tsx # App layout
| | | — package.json # Dependencies including @repo/db and @repo/tasks
| |
| | — docs/ # Documentation app (not fully implemented)
|
| — packages/
| | — database/ # Prisma database package (@repo/db)
| | | — prisma/
| | | | — schema.prisma # Database schema definition
| | | — generated/ # Generated Prisma client (gitignored)
| | | — src/
| | | | — index.ts # Exports from the database package
| | | — package.json # Database package dependencies
| |
| | — tasks/ # Trigger.dev tasks package (@repo/tasks)
| | | — src/
| | | | — index.ts # Exports from the tasks package
| | | | — trigger/
| | | | — index.ts # Exports the tasks
| | | | — addNewUser.ts # Task implementation for adding users
| | | — trigger.config.ts # Trigger.dev configuration
| | | — package.json # Tasks package dependencies
| |
| | — ui/ # UI components package (referenced but not detailed)
|
| — turbo.json # Turborepo configuration
| — package.json # Root package.json with workspace config
```
### Relevant files and code
#### Database package
- Prisma is added as a package in [`/packages/database`](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-package/packages/database/) and exported as `@repo/db` in the [`package.json`](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-package/packages/database/package.json) file.
- The schema is defined in the [`prisma/schema.prisma`](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-package/packages/database/prisma/schema.prisma) file.
#### Tasks package
<Note>
to run `pnpm dlx trigger.dev@latest init` in a blank packages folder, you have to add a `package.json` file first, otherwise it will attempt to add Trigger.dev files in the root of your monorepo.
</Note>
- Trigger.dev is added as a package in [`/packages/tasks`](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-package/packages/tasks) and exported as `@repo/tasks` in the [`package.json`](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-package/packages/tasks/package.json) file.
- The [`addNewUser.ts`](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-package/packages/tasks/src/trigger/addNewUser.ts) task adds a new user to the database.
- The [`packages/tasks/src/index.ts`](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-package/packages/tasks/src/index.ts) file exports values and types from the Trigger.dev SDK, and is exported from the package via the [`package.json`](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-package/packages/tasks/package.json) file.
- The [`packages/tasks/src/trigger/index.ts`](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-package/packages/tasks/src/trigger/index.ts) file exports the task from the package. Every task must be exported from the package like this.
- The [`trigger.config.ts`](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-package/packages/tasks/trigger.config.ts) file configures the Trigger.dev project settings. This is where the Trigger.dev [Prisma build extension](https://trigger.dev/docs/config/extensions/prismaExtension) is added, which is required to use Prisma in the Trigger.dev task.
<Info>
You must include the version of Prisma you are using in the `trigger.config.ts` file, otherwise the Prisma build extension will not work. Learn more about our [Prisma build extension](/config/extensions/prismaExtension).
</Info>
#### The Next.js app `apps/web`
- The app is a simple Next.js app using the App Router, that uses the `@repo/db` package to interact with the database and the `@repo/tasks` package to trigger the task. These are both added as dependencies in the [`package.json`](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-package/apps/web/package.json) file.
- The task is triggered from a button click in the app in [`page.tsx`](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-package/apps/web/app/page.tsx), which uses a server action in [`/app/api/actions.ts`](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-package/apps/web/app/api/actions.ts) to trigger the task with an example payload.
### Running the example
To run this example, check out the full instructions [in the GitHub repo README file](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-package/README.md).
## Example 2: Turborepo monorepo demo with a Prisma package and Trigger.dev installed in a Next.js app
This example demonstrates how to use Trigger.dev and Prisma in a monorepo created with Turborepo. Prisma has been added as a package, and Trigger.dev has been installed in a Next.js app. The task is triggered by a button click in the app via a server action.
### GitHub repo
Fork the GitHub repo below to get started with this example project.
<Card
title="Check out the Turborepo monorepo demo with a Prisma package and Trigger.dev installed in a Next.js app"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-trigger"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
### Features
- This monorepo has been created using the [Turborepo CLI](https://turbo.build/repo), following the official [Prisma and Turborepo docs](https://www.prisma.io/docs/guides/turborepo), and then adapted for use with Trigger.dev.
- [pnpm](https://pnpm.io/) has been used as the package manager.
- A database package (`@repo/db`) using [Prisma ORM](https://www.prisma.io/docs/orm/) is used to interact with the database. You can use any popular Postgres database supported by Prisma, e.g. [Supabase](https://supabase.com/), [Neon](https://neon.tech/), etc.
- A [Next.js](https://nextjs.org/) example app (`apps/web`) to show how to trigger the task via a server action.
- Trigger.dev initialized and an `addNewUser` task created in the `web` app.
### Project structure
Simplified project structure for this example:
```
|
| — apps/
| | — web/ # Next.js frontend application
| | | — app/ # Next.js app router
| | | | — api/
| | | | | — actions.ts # Server actions for triggering tasks
| | | | — page.tsx # Main page with "Add new user" button
| | | — src/
| | | | — trigger/
| | | | — addNewUser.ts # Task implementation for adding users
| | | — trigger.config.ts # Trigger.dev configuration
| | | — package.json # Dependencies including @repo/db
| |
| | — docs/ # Documentation app
| | — app/
| | — page.tsx # Docs landing page
|
| — packages/
| | — database/ # Prisma database package (@repo/db)
| | | — prisma/
| | | | — schema.prisma # Database schema definition
| |
| | — ui/ # UI components package
|
| — turbo.json # Turborepo configuration
| — package.json # Root package.json with workspace config
```
## Relevant files and code
### Database package (`@repo/db`)
- Located in [`/packages/database/`](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-trigger/packages/database/) and exported as `@repo/db`
- Schema defined in [`schema.prisma`](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-trigger/packages/database/prisma/schema.prisma)
- Provides database access to other packages and apps
### Next.js app (`apps/web`)
- Contains Trigger.dev configuration in [`trigger.config.ts`](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-trigger/apps/web/trigger.config.ts)
- Trigger.dev tasks are defined in [`src/trigger/`](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-trigger/apps/web/src/trigger/) (e.g., [`addNewUser.ts`](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-trigger/apps/web/src/trigger/addNewUser.ts))
- Demonstrates triggering tasks via server actions in [`app/api/actions.ts`](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-trigger/apps/web/app/api/actions.ts)
### Running the example
To run this example, check out the full instructions [in the GitHub repo README file](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-trigger/README.md).
@@ -0,0 +1,138 @@
---
title: "Deep research agent using Vercel's AI SDK"
sidebarTitle: "Deep research agent"
description: "Deep research agent which generates comprehensive PDF reports using Vercel's AI SDK."
---
import RealtimeLearnMore from "/snippets/realtime-learn-more.mdx";
<Info title="Acknowledgements">
Acknowledgements: This example project is derived from the brilliant [deep research
guide](https://aie-feb-25.vercel.app/docs/deep-research) by [Nico
Albanese](https://x.com/nicoalbanese10).
</Info>
## Overview
This full-stack project is an intelligent deep research agent that autonomously conducts multi-layered web research, generating comprehensive reports which are then converted to PDF and uploaded to storage.
<video
controls
className="w-full aspect-video"
src="https://github.com/user-attachments/assets/aa86d2b2-7aa7-4948-82ff-5e1e80cf8e37"
></video>
**Tech stack:**
- **[Next.js](https://nextjs.org/)** for the web app
- **[Vercel's AI SDK](https://sdk.vercel.ai/)** for AI model integration and structured generation
- **[Trigger.dev](https://trigger.dev)** for task orchestration, execution and real-time progress updates
- **[OpenAI's GPT-4o model](https://openai.com/gpt-4)** for intelligent query generation, content analysis, and report creation
- **[Exa API](https://exa.ai/)** for semantic web search with live crawling
- **[LibreOffice](https://www.libreoffice.org/)** for PDF generation
- **[Cloudflare R2](https://developers.cloudflare.com/r2/)** to store the generated reports
**Features:**
- **Recursive research**: AI generates search queries, evaluates their relevance, asks follow-up questions and searches deeper based on initial findings.
- **Real-time progress**: Live updates are shown on the frontend using Trigger.dev Realtime as research progresses.
- **Intelligent source evaluation**: AI evaluates search result relevance before processing.
- **Research report generation**: The completed research is converted to a structured HTML report using a detailed system prompt.
- **PDF creation and uploading to Cloud storage**: The completed reports are then converted to PDF using LibreOffice and uploaded to Cloudflare R2.
## GitHub repo
<Card
title="View the Vercel AI SDK deep research agent repo"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/vercel-ai-sdk-deep-research-agent"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## How the deep research agent works
### Trigger.dev orchestration
The research process is orchestrated through three connected Trigger.dev tasks:
1. `deepResearchOrchestrator` - Main task that coordinates the entire research workflow.
2. `generateReport` - Processes research data into a structured HTML report using OpenAI's GPT-4o model
3. `generatePdfAndUpload` - Converts HTML to PDF using LibreOffice and uploads to R2 cloud storage
Each task uses `triggerAndWait()` to create a dependency chain, ensuring proper sequencing while maintaining isolation and error handling.
### The deep research recursive function
The core research logic uses a recursive depth-first search approach. A query is recursively expanded and the results are collected.
**Key parameters:**
- `depth`: Controls recursion levels (default: 2)
- `breadth`: Number of queries per level (default: 2, halved each recursion)
```
Level 0 (Initial Query): "AI safety in autonomous vehicles"
├── Level 1 (depth = 1, breadth = 2):
│ ├── Sub-query 1: "Machine learning safety protocols in self-driving cars"
│ │ ├── → Search Web → Evaluate Relevance → Extract Learnings
│ │ └── → Follow-up: "How do neural networks handle edge cases?"
│ │
│ └── Sub-query 2: "Regulatory frameworks for autonomous vehicle testing"
│ ├── → Search Web → Evaluate Relevance → Extract Learnings
│ └── → Follow-up: "What are current safety certification requirements?"
└── Level 2 (depth = 2, breadth = 1):
├── From Sub-query 1 follow-up:
│ └── "Neural network edge case handling in autonomous systems"
│ └── → Search Web → Evaluate → Extract → DEPTH LIMIT REACHED
└── From Sub-query 2 follow-up:
└── "Safety certification requirements for self-driving vehicles"
└── → Search Web → Evaluate → Extract → DEPTH LIMIT REACHED
```
**Process flow:**
1. **Query generation**: OpenAI's GPT-4o generates multiple search queries from the input
2. **Web search**: Each query searches the web via the Exa API with live crawling
3. **Relevance evaluation**: OpenAI's GPT-4o evaluates if results help answer the query
4. **Learning extraction**: Relevant results are analyzed for key insights and follow-up questions
5. **Recursive deepening**: Follow-up questions become new queries for the next depth level
6. **Accumulation**: All learnings, sources, and queries are accumulated across recursion levels
### Using Trigger.dev Realtime to trigger and subscribe to the deep research task
We use the [`useRealtimeTaskTrigger`](/realtime/react-hooks/triggering#userealtimetasktrigger) React hook to trigger the `deep-research` task and subscribe to it's updates.
**Frontend (React Hook)**:
```typescript
const triggerInstance = useRealtimeTaskTrigger<typeof deepResearchOrchestrator>("deep-research", {
accessToken: triggerToken,
});
const { progress, label } = parseStatus(triggerInstance.run?.metadata);
```
As the research progresses, the metadata is set within the tasks and the frontend is kept updated with every new status:
**Task Metadata**:
```typescript
metadata.set("status", {
progress: 25,
label: `Searching the web for: "${query}"`,
});
```
## Relevant code
- **Deep research task**: Core logic in [src/trigger/deepResearch.ts](https://github.com/triggerdotdev/examples/blob/main/vercel-ai-sdk-deep-research-agent/src/trigger/deepResearch.ts) - orchestrates the recursive research process. Here you can change the model, the depth and the breadth of the research.
- **Report generation**: [src/trigger/generateReport.ts](https://github.com/triggerdotdev/examples/blob/main/vercel-ai-sdk-deep-research-agent/src/trigger/generateReport.ts) - creates structured HTML reports from research data. The system prompt is defined in the code - this can be updated to be more or less detailed.
- **PDF generation**: [src/trigger/generatePdfAndUpload.ts](https://github.com/triggerdotdev/examples/blob/main/vercel-ai-sdk-deep-research-agent/src/trigger/generatePdfAndUpload.ts) - converts reports to PDF and uploads to R2. This is a simple example of how to use LibreOffice to convert HTML to PDF.
- **Research agent UI**: [src/components/DeepResearchAgent.tsx](https://github.com/triggerdotdev/examples/blob/main/vercel-ai-sdk-deep-research-agent/src/components/DeepResearchAgent.tsx) - handles form submission and real-time progress display using the `useRealtimeTaskTrigger` hook.
- **Progress component**: [src/components/progress-section.tsx](https://github.com/triggerdotdev/examples/blob/main/deep-research-agent/src/components/progress-section.tsx) - displays live research progress.
<RealtimeLearnMore />
@@ -0,0 +1,41 @@
---
title: "Vercel AI SDK image generator"
sidebarTitle: "Vercel AI SDK image generator"
description: "This example Next.js project uses the Vercel AI SDK to generate images from a prompt."
---
import RealtimeLearnMore from "/snippets/realtime-learn-more.mdx";
## Overview
This demo is a full stack example that uses the following:
- A [Next.js](https://nextjs.org/) app using [shadcn](https://ui.shadcn.com/) for the UI
- Our 'useRealtimeRun' [React hook](/realtime/react-hooks/subscribe#userealtimerun) to subscribe to the run and show updates on the frontend
- The [Vercel AI SDK](https://sdk.vercel.ai/docs/introduction) to [generate images](https://sdk.vercel.ai/docs/ai-sdk-core/image-generation) using OpenAI's DALL-E models
## GitHub repo
<Card
title="View the Vercel AI SDK image generator repo"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/vercel-ai-sdk-image-generator"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## Video
<video
controls
className="w-full aspect-video"
src="https://github.com/user-attachments/assets/960edcb6-e225-4983-a48c-6fa697295dec"
></video>
## Relevant code
- View the Trigger.dev task code which generates the image using the Vercel AI SDK in [src/trigger/realtime-generate-image.ts](https://github.com/triggerdotdev/examples/tree/main/vercel-ai-sdk-image-generator/src/trigger/realtime-generate-image.ts).
- We use a [useRealtimeRun](/realtime/react-hooks/subscribe#userealtimerun) hook to subscribe to the run in [src/app/processing/[id]/ProcessingContent.tsx](https://github.com/triggerdotdev/examples/tree/main/vercel-ai-sdk-image-generator/src/app/processing/[id]/ProcessingContent.tsx).
<RealtimeLearnMore />
@@ -0,0 +1,77 @@
---
title: "Generate an image using DALL·E 3"
sidebarTitle: "DALL·E image generation"
description: "This example will show you how to generate an image using DALL·E 3 and text using GPT-4o with Trigger.dev."
---
## Overview
This example demonstrates how to use Trigger.dev to make reliable calls to AI APIs, specifically OpenAI's GPT-4o and DALL-E 3. It showcases automatic retrying with a maximum of 3 attempts, built-in error handling to avoid timeouts, and the ability to trace and monitor API calls.
## Task code
```ts trigger/generateContent.ts
import { task } from "@trigger.dev/sdk";
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
type Payload = {
theme: string;
description: string;
};
export const generateContent = task({
id: "generate-content",
retry: {
maxAttempts: 3, // Retry up to 3 times
},
run: async ({ theme, description }: Payload) => {
// Generate text
const textResult = await openai.chat.completions.create({
model: "gpt-4o",
messages: generateTextPrompt(theme, description),
});
if (!textResult.choices[0]) {
throw new Error("No content, retrying…");
}
// Generate image
const imageResult = await openai.images.generate({
model: "dall-e-3",
prompt: generateImagePrompt(theme, description),
});
if (!imageResult.data[0]) {
throw new Error("No image, retrying…");
}
return {
text: textResult.choices[0],
image: imageResult.data[0].url,
};
},
});
function generateTextPrompt(theme: string, description: string): any {
return `Theme: ${theme}\n\nDescription: ${description}`;
}
function generateImagePrompt(theme: string, description: string): any {
return `Theme: ${theme}\n\nDescription: ${description}`;
}
```
## Testing your task
To test this task in the dashboard, you can use the following payload:
```json
{
"theme": "A beautiful sunset",
"description": "A sunset over the ocean with a tiny yacht in the distance."
}
```
@@ -0,0 +1,71 @@
---
title: "Transcribe audio using Deepgram"
sidebarTitle: "Deepgram audio transcription"
description: "This example will show you how to transcribe audio using Deepgram's speech recognition API with Trigger.dev."
---
## Overview
Transcribe audio using [Deepgram's](https://developers.deepgram.com/docs/introduction) speech recognition API.
## Key Features
- Transcribe audio from a URL
- Use the Nova 2 model for transcription
## Task code
```ts trigger/deepgramTranscription.ts
import { createClient } from "@deepgram/sdk";
import { logger, task } from "@trigger.dev/sdk";
// Initialize the Deepgram client, using your Deepgram API key (you can find this in your Deepgram account settings).
const deepgram = createClient(process.env.DEEPGRAM_SECRET_KEY);
export const deepgramTranscription = task({
id: "deepgram-transcribe-audio",
run: async (payload: { audioUrl: string }) => {
const { audioUrl } = payload;
logger.log("Transcribing audio from URL", { audioUrl });
// Transcribe the audio using Deepgram
const { result, error } = await deepgram.listen.prerecorded.transcribeUrl(
{
url: audioUrl,
},
{
model: "nova-2", // Use the Nova 2 model for the transcription
smart_format: true, // Automatically format transcriptions to improve readability
diarize: true, // Recognize speaker changes and assign a speaker to each word in the transcript
}
);
if (error) {
logger.error("Failed to transcribe audio", { error });
throw error;
}
console.dir(result, { depth: null });
// Extract the transcription from the result
const transcription = result.results.channels[0].alternatives[0].paragraphs?.transcript;
logger.log(`Generated transcription: ${transcription}`);
return {
result,
};
},
});
```
## Testing your task
To test this task in the dashboard, you can use the following payload:
```json
{
"audioUrl": "https://dpgr.am/spacewalk.wav"
}
```
@@ -0,0 +1,111 @@
---
title: "Convert an image to a cartoon using Fal.ai"
sidebarTitle: "Fal.ai image to cartoon"
description: "This example task generates an image from a URL using Fal.ai and uploads it to Cloudflare R2."
---
## Walkthrough
This video walks through the process of creating this task in a Next.js project.
<iframe
width="100%"
height="315"
src="https://www.youtube.com/embed/AyRT4X8dHK0?si=ugA172V_3TMjik9h"
title="Trigger.dev walkthrough"
allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerPolicy="strict-origin-when-cross-origin"
allowFullScreen
/>
## Prerequisites
- An existing project
- A [Trigger.dev account](https://cloud.trigger.dev) with Trigger.dev [initialized in your project](/quick-start)
- A [Fal.ai](https://fal.ai/) account
- A [Cloudflare](https://developers.cloudflare.com/r2/) account with an R2 bucket setup
## Task code
This task converts an image to a cartoon using Fal.ai, and uploads the result to Cloudflare R2.
```ts trigger/fal-ai-image-to-cartoon.ts
import { logger, task } from "@trigger.dev/sdk";
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import * as fal from "@fal-ai/serverless-client";
import fetch from "node-fetch";
import { z } from "zod";
// Initialize fal.ai client
fal.config({
credentials: process.env.FAL_KEY, // Get this from your fal.ai dashboard
});
// Initialize S3-compatible client for Cloudflare R2
const s3Client = new S3Client({
// How to authenticate to R2: https://developers.cloudflare.com/r2/api/s3/tokens/
region: "auto",
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "",
},
});
export const FalResult = z.object({
images: z.tuple([z.object({ url: z.string() })]),
});
export const falAiImageToCartoon = task({
id: "fal-ai-image-to-cartoon",
run: async (payload: { imageUrl: string; fileName: string }) => {
logger.log("Converting image to cartoon", payload);
// Convert image to cartoon using fal.ai
const result = await fal.subscribe("fal-ai/flux/dev/image-to-image", {
input: {
prompt: "Turn the image into a cartoon in the style of a Pixar character",
image_url: payload.imageUrl,
},
onQueueUpdate: (update) => {
logger.info("Fal.ai processing update", { update });
},
});
const $result = FalResult.parse(result);
const [{ url: cartoonImageUrl }] = $result.images;
// Download the cartoon image
const imageResponse = await fetch(cartoonImageUrl);
const imageBuffer = await imageResponse.arrayBuffer().then(Buffer.from);
// Upload to Cloudflare R2
const r2Key = `cartoons/${payload.fileName}`;
const uploadParams = {
Bucket: process.env.R2_BUCKET, // Create a bucket in your Cloudflare dashboard
Key: r2Key,
Body: imageBuffer,
ContentType: "image/png",
};
logger.log("Uploading cartoon to R2", { key: r2Key });
await s3Client.send(new PutObjectCommand(uploadParams));
logger.log("Cartoon uploaded to R2", { key: r2Key });
return {
originalUrl: payload.imageUrl,
cartoonUrl: `File uploaded to storage at: ${r2Key}`,
};
},
});
```
### Testing your task
You can test your task by triggering it from the Trigger.dev dashboard.
```json
"imageUrl": "<image-url>", // Replace with the URL of the image you want to convert to a cartoon
"fileName": "<file-name>" // Replace with the name you want to save the file as in Cloudflare R2
```
+89
View File
@@ -0,0 +1,89 @@
---
title: "Generate an image from a prompt using Fal.ai and Trigger.dev Realtime"
sidebarTitle: "Fal.ai with Realtime"
description: "This example task generates an image from a prompt using Fal.ai and shows the progress of the task on the frontend using Trigger.dev Realtime."
---
## GitHub repo
<Card
title="View the project on GitHub"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/realtime-fal-ai-image-generation"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## Walkthrough
This video walks through the process of creating this task in a Next.js project.
<iframe
width="100%"
height="315"
src="https://www.youtube.com/embed/BWZqYfUaigg?si=XpqVUEIf1j4bsYZ4"
title="Trigger.dev walkthrough"
allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerPolicy="strict-origin-when-cross-origin"
allowFullScreen
/>
## Prerequisites
- An existing project
- A [Trigger.dev account](https://cloud.trigger.dev) with Trigger.dev [initialized in your project](/quick-start)
- A [Fal.ai](https://fal.ai/) account
## Task code
This task generates an image from a prompt using Fal.ai.
```ts trigger/fal-ai-image-from-prompt-realtime.ts
import * as fal from "@fal-ai/serverless-client";
import { logger, schemaTask } from "@trigger.dev/sdk";
import { z } from "zod";
export const FalResult = z.object({
images: z.tuple([z.object({ url: z.string() })]),
});
export const payloadSchema = z.object({
imageUrl: z.string().url(),
prompt: z.string(),
});
export const realtimeImageGeneration = schemaTask({
id: "realtime-image-generation",
schema: payloadSchema,
run: async (payload) => {
const result = await fal.subscribe("fal-ai/flux/dev/image-to-image", {
input: {
image_url: payload.imageUrl,
prompt: payload.prompt,
},
onQueueUpdate: (update) => {
logger.info("Fal.ai processing update", { update });
},
});
const $result = FalResult.parse(result);
const [{ url: cartoonUrl }] = $result.images;
return {
imageUrl: cartoonUrl,
};
},
});
```
### Testing your task
You can test your task by triggering it from the Trigger.dev dashboard. Here's an example payload:
```json
{
"imageUrl": "https://static.vecteezy.com/system/resources/previews/005/857/332/non_2x/funny-portrait-of-cute-corgi-dog-outdoors-free-photo.jpg",
"prompt": "Dress this dog for Christmas"
}
```
@@ -0,0 +1,371 @@
---
title: "Video processing with FFmpeg"
sidebarTitle: "FFmpeg video processing"
description: "These examples show you how to process videos in various ways using FFmpeg with Trigger.dev."
---
import LocalDevelopment from "/snippets/local-development-extensions.mdx";
## Prerequisites
- A project with [Trigger.dev initialized](/quick-start)
- [FFmpeg](https://www.ffmpeg.org/download.html) installed on your machine
### Adding the FFmpeg build extension
To use these example tasks, you'll first need to add our FFmpeg extension to your project configuration like this:
```ts trigger.config.ts
import { ffmpeg } from "@trigger.dev/build/extensions/core";
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
project: "<project ref>",
// Your other config settings...
build: {
extensions: [ffmpeg()],
},
});
```
<Note>
[Build extensions](/config/extensions/overview) allow you to hook into the build system and
customize the build process or the resulting bundle and container image (in the case of
deploying). You can use pre-built extensions or create your own.
</Note>
You'll also need to add `@trigger.dev/build` to your `package.json` file under `devDependencies` if you don't already have it there.
If you are modifying this example and using popular FFmpeg libraries like `fluent-ffmpeg` you'll also need to add them to [`external`](/config/config-file#external) in your `trigger.config.ts` file.
## Compress a video using FFmpeg
This task demonstrates how to use FFmpeg to compress a video, reducing its file size while maintaining reasonable quality, and upload the compressed video to R2 storage.
### Key Features
- Fetches a video from a given URL
- Compresses the video using FFmpeg with various compression settings
- Uploads the compressed video to R2 storage
### Task code
```ts trigger/ffmpeg-compress-video.ts
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { logger, task } from "@trigger.dev/sdk";
import ffmpeg from "fluent-ffmpeg";
import fs from "fs/promises";
import fetch from "node-fetch";
import { Readable } from "node:stream";
import os from "os";
import path from "path";
// Initialize S3 client
const s3Client = new S3Client({
// How to authenticate to R2: https://developers.cloudflare.com/r2/api/s3/tokens/
region: "auto",
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "",
},
});
export const ffmpegCompressVideo = task({
id: "ffmpeg-compress-video",
run: async (payload: { videoUrl: string }) => {
const { videoUrl } = payload;
// Generate temporary file names
const tempDirectory = os.tmpdir();
const outputPath = path.join(tempDirectory, `output_${Date.now()}.mp4`);
// Fetch the video
const response = await fetch(videoUrl);
// Compress the video
await new Promise((resolve, reject) => {
if (!response.body) {
return reject(new Error("Failed to fetch video"));
}
ffmpeg(Readable.from(response.body))
.outputOptions([
"-c:v libx264", // Use H.264 codec
"-crf 28", // Higher CRF for more compression (28 is near the upper limit for acceptable quality)
"-preset veryslow", // Slowest preset for best compression
"-vf scale=iw/2:ih/2", // Reduce resolution to 320p width (height auto-calculated)
"-c:a aac", // Use AAC for audio
"-b:a 64k", // Reduce audio bitrate to 64k
"-ac 1", // Convert to mono audio
])
.output(outputPath)
.on("end", resolve)
.on("error", reject)
.run();
});
// Read the compressed video
const compressedVideo = await fs.readFile(outputPath);
const compressedSize = compressedVideo.length;
// Log compression results
logger.log(`Compressed video size: ${compressedSize} bytes`);
logger.log(`Temporary compressed video file created`, { outputPath });
// Create the r2Key for the extracted audio, using the base name of the output path
const r2Key = `processed-videos/${path.basename(outputPath)}`;
const uploadParams = {
Bucket: process.env.R2_BUCKET,
Key: r2Key,
Body: compressedVideo,
};
// Upload the video to R2 and get the URL
await s3Client.send(new PutObjectCommand(uploadParams));
logger.log(`Compressed video saved to your r2 bucket`, { r2Key });
// Delete the temporary compressed video file
await fs.unlink(outputPath);
logger.log(`Temporary compressed video file deleted`, { outputPath });
// Return the compressed video buffer and r2 key
return {
Bucket: process.env.R2_BUCKET,
r2Key,
};
},
});
```
### Testing your task
To test this task, use this payload structure:
```json
{
"videoUrl": "<video-url>" // Replace <a-video-url> with the URL of the video you want to upload
}
```
## Extract audio from a video using FFmpeg
This task demonstrates how to use FFmpeg to extract audio from a video, convert it to WAV format, and upload it to R2 storage.
### Key Features
- Fetches a video from a given URL
- Extracts the audio from the video using FFmpeg
- Converts the extracted audio to WAV format
- Uploads the extracted audio to R2 storage
### Task code
```ts trigger/ffmpeg-extract-audio.ts
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { logger, task } from "@trigger.dev/sdk";
import ffmpeg from "fluent-ffmpeg";
import fs from "fs/promises";
import fetch from "node-fetch";
import { Readable } from "node:stream";
import os from "os";
import path from "path";
// Initialize S3 client
const s3Client = new S3Client({
// How to authenticate to R2: https://developers.cloudflare.com/r2/api/s3/tokens/
region: "auto",
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "",
},
});
export const ffmpegExtractAudio = task({
id: "ffmpeg-extract-audio",
run: async (payload: { videoUrl: string }) => {
const { videoUrl } = payload;
// Generate temporary file names
const tempDirectory = os.tmpdir();
const outputPath = path.join(tempDirectory, `audio_${Date.now()}.wav`);
// Fetch the video
const response = await fetch(videoUrl);
// Extract the audio
await new Promise((resolve, reject) => {
if (!response.body) {
return reject(new Error("Failed to fetch video"));
}
ffmpeg(Readable.from(response.body))
.outputOptions([
"-vn", // Disable video output
"-acodec pcm_s16le", // Use PCM 16-bit little-endian encoding
"-ar 44100", // Set audio sample rate to 44.1 kHz
"-ac 2", // Set audio channels to stereo
])
.output(outputPath)
.on("end", resolve)
.on("error", reject)
.run();
});
// Read the extracted audio
const audioBuffer = await fs.readFile(outputPath);
const audioSize = audioBuffer.length;
// Log audio extraction results
logger.log(`Extracted audio size: ${audioSize} bytes`);
logger.log(`Temporary audio file created`, { outputPath });
// Create the r2Key for the extracted audio, using the base name of the output path
const r2Key = `extracted-audio/${path.basename(outputPath)}`;
const uploadParams = {
Bucket: process.env.R2_BUCKET,
Key: r2Key,
Body: audioBuffer,
};
// Upload the audio to R2 and get the URL
await s3Client.send(new PutObjectCommand(uploadParams));
logger.log(`Extracted audio saved to your R2 bucket`, { r2Key });
// Delete the temporary audio file
await fs.unlink(outputPath);
logger.log(`Temporary audio file deleted`, { outputPath });
// Return the audio file path, size, and R2 URL
return {
Bucket: process.env.R2_BUCKET,
r2Key,
};
},
});
```
### Testing your task
To test this task, use this payload structure:
<Warning>
Make sure to provide a video URL that contains audio. If the video does not have audio, the task
will fail.
</Warning>
```json
{
"videoUrl": "<video-url>" // Replace <a-video-url> with the URL of the video you want to upload
}
```
## Generate a thumbnail from a video using FFmpeg
This task demonstrates how to use FFmpeg to generate a thumbnail from a video at a specific time and upload the generated thumbnail to R2 storage.
### Key Features
- Fetches a video from a given URL
- Generates a thumbnail from the video at the 5-second mark
- Uploads the generated thumbnail to R2 storage
### Task code
```ts trigger/ffmpeg-generate-thumbnail.ts
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { logger, task } from "@trigger.dev/sdk";
import ffmpeg from "fluent-ffmpeg";
import fs from "fs/promises";
import fetch from "node-fetch";
import { Readable } from "node:stream";
import os from "os";
import path from "path";
// Initialize S3 client
const s3Client = new S3Client({
// How to authenticate to R2: https://developers.cloudflare.com/r2/api/s3/tokens/
region: "auto",
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "",
},
});
export const ffmpegGenerateThumbnail = task({
id: "ffmpeg-generate-thumbnail",
run: async (payload: { videoUrl: string }) => {
const { videoUrl } = payload;
// Generate output file name
const tempDirectory = os.tmpdir();
const outputPath = path.join(tempDirectory, `thumbnail_${Date.now()}.jpg`);
// Fetch the video
const response = await fetch(videoUrl);
// Generate the thumbnail
await new Promise((resolve, reject) => {
if (!response.body) {
return reject(new Error("Failed to fetch video"));
}
ffmpeg(Readable.from(response.body))
.screenshots({
count: 1,
folder: "/tmp",
filename: path.basename(outputPath),
size: "320x240",
timemarks: ["5"], // 5 seconds
})
.on("end", resolve)
.on("error", reject);
});
// Read the generated thumbnail
const thumbnail = await fs.readFile(outputPath);
// Create the r2Key for the extracted audio, using the base name of the output path
const r2Key = `thumbnails/${path.basename(outputPath)}`;
const uploadParams = {
Bucket: process.env.R2_BUCKET,
Key: r2Key,
Body: thumbnail,
};
// Upload the thumbnail to R2 and get the URL
await s3Client.send(new PutObjectCommand(uploadParams));
const r2Url = `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET}/${r2Key}`;
logger.log("Thumbnail uploaded to R2", { url: r2Url });
// Delete the temporary file
await fs.unlink(outputPath);
// Log thumbnail generation results
logger.log(`Thumbnail uploaded to S3: ${r2Url}`);
// Return the thumbnail buffer, path, and R2 URL
return {
thumbnailBuffer: thumbnail,
thumbnailPath: outputPath,
r2Url,
};
},
});
```
### Testing your task
To test this task in the dashboard, you can use the following payload:
```json
{
"videoUrl": "<video-url>" // Replace <a-video-url> with the URL of the video you want to upload
}
```
<LocalDevelopment packages={"ffmpeg"} />
@@ -0,0 +1,99 @@
---
title: "Crawl a URL using Firecrawl"
sidebarTitle: "Firecrawl URL crawl"
description: "This example demonstrates how to crawl a URL using Firecrawl with Trigger.dev."
---
## Overview
Firecrawl is a tool for crawling websites and extracting clean markdown that's structured in an LLM-ready format.
Here are two examples of how to use Firecrawl with Trigger.dev:
## Prerequisites
- A project with [Trigger.dev initialized](/quick-start)
- A [Firecrawl](https://firecrawl.dev/) account
## Example 1: crawl an entire website with Firecrawl
This task crawls a website and returns the `crawlResult` object. You can set the `limit` parameter to control the number of URLs that are crawled.
```ts trigger/firecrawl-url-crawl.ts
import Firecrawl from "@mendable/firecrawl-js";
import { task } from "@trigger.dev/sdk";
// Initialize the Firecrawl client with your API key
const firecrawlClient = new Firecrawl({
apiKey: process.env.FIRECRAWL_API_KEY, // Get this from your Firecrawl dashboard
});
export const firecrawlCrawl = task({
id: "firecrawl-crawl",
run: async (payload: { url: string }) => {
const { url } = payload;
// Crawl: scrapes all the URLs of a web page and return content in LLM-ready format
const crawlResult = await firecrawlClient.crawl(url, {
limit: 100, // Limit the number of URLs to crawl
scrapeOptions: {
formats: ["markdown", "html"],
},
});
if (crawlResult.status === "failed") {
throw new Error(`Failed to crawl: ${url}`);
}
return {
data: crawlResult,
};
},
});
```
### Testing your task
You can test your task by triggering it from the Trigger.dev dashboard.
```json
"url": "<url-to-crawl>" // Replace with the URL you want to crawl
```
## Example 2: scrape a single URL with Firecrawl
This task scrapes a single URL and returns the `scrapeResult` object.
```ts trigger/firecrawl-url-scrape.ts
import Firecrawl from "@mendable/firecrawl-js";
import { task } from "@trigger.dev/sdk";
// Initialize the Firecrawl client with your API key
const firecrawlClient = new Firecrawl({
apiKey: process.env.FIRECRAWL_API_KEY, // Get this from your Firecrawl dashboard
});
export const firecrawlScrape = task({
id: "firecrawl-scrape",
run: async (payload: { url: string }) => {
const { url } = payload;
// Scrape: scrapes a URL and get its content in LLM-ready format (markdown, structured data via LLM Extract, screenshot, html)
const scrapeResult = await firecrawlClient.scrape(url, {
formats: ["markdown", "html"],
});
return {
data: scrapeResult,
};
},
});
```
### Testing your task
You can test your task by triggering it from the Trigger.dev dashboard.
```json
"url": "<url-to-scrape>" // Replace with the URL you want to scrape
```
+71
View File
@@ -0,0 +1,71 @@
---
title: "Trigger tasks from Hookdeck webhooks"
sidebarTitle: "Hookdeck webhooks"
description: "This example demonstrates how to use Hookdeck to receive webhooks and trigger Trigger.dev tasks."
---
## Overview
This example shows how to use [Hookdeck](https://hookdeck.com) as your webhook infrastructure to trigger Trigger.dev tasks. Hookdeck receives webhooks from external services, and forwards them directly to the Trigger.dev API. This gives you the best of both worlds: Hookdeck's webhook management, logging, and replay capabilities, combined with Trigger.dev's reliable task execution.
## Key features
- Use Hookdeck as your webhook endpoint for external services
- Hookdeck forwards webhooks directly to Trigger.dev tasks via the API
- All webhooks are logged and replayable in Hookdeck
## Setting up Hookdeck
You'll configure everything in the [Hookdeck dashboard](https://dashboard.hookdeck.com). No code changes needed in your app.
### 1. Create a destination
In Hookdeck, create a new [destination](https://hookdeck.com/docs/destinations) with the following settings:
- **URL**: `https://api.trigger.dev/api/v1/tasks/<task-id>/trigger` (replace `<task-id>` with your task ID)
- **Method**: POST
- **Authentication**: Bearer token (use your `TRIGGER_SECRET_KEY` from Trigger.dev)
### 2. Add a transformation
Create a [transformation](https://hookdeck.com/docs/transformations) to wrap the webhook body in the `payload` field that Trigger.dev expects:
```javascript
addHandler("transform", (request, context) => {
request.body = { payload: { ...request.body } };
return request;
});
```
### 3. Create a connection
Create a [connection](https://hookdeck.com/docs/connections) that links your source (where webhooks come from) to the destination and transformation you created above.
## Task code
This task will be triggered when Hookdeck forwards a webhook to the Trigger.dev API.
```ts trigger/webhook-handler.ts
import { task } from "@trigger.dev/sdk";
export const webhookHandler = task({
id: "webhook-handler",
run: async (payload: Record<string, unknown>) => {
// The payload contains the original webhook data from the external service
console.log("Received webhook:", payload);
// Add your custom logic here
},
});
```
## Testing your setup
To test everything is working:
1. Set up your destination, transformation, and connection in [Hookdeck](https://dashboard.hookdeck.com)
2. Send a test webhook to your Hookdeck source URL (use the Hookdeck Console or cURL)
3. Check the Hookdeck dashboard to verify the webhook was received and forwarded
4. Check the [Trigger.dev dashboard](https://cloud.trigger.dev) to see the successful run of your task
For more information on setting up Hookdeck, refer to the [Hookdeck Documentation](https://hookdeck.com/docs).
@@ -0,0 +1,133 @@
---
title: "Convert documents to PDF using LibreOffice"
sidebarTitle: "LibreOffice PDF conversion"
description: "This example demonstrates how to convert documents to PDF using LibreOffice with Trigger.dev."
---
import LocalDevelopment from "/snippets/local-development-extensions.mdx";
## Prerequisites
- A project with [Trigger.dev initialized](/quick-start)
- [LibreOffice](https://www.libreoffice.org/download/download-libreoffice/) installed on your machine
- A [Cloudflare R2](https://developers.cloudflare.com) account and bucket
### Using our `aptGet` build extension to add the LibreOffice package
To deploy this task, you'll need to add LibreOffice to your project configuration, like this:
```ts trigger.config.ts
import { aptGet } from "@trigger.dev/build/extensions/core";
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
project: "<project ref>",
// Your other config settings...
build: {
extensions: [
aptGet({
packages: ["libreoffice"],
}),
],
},
});
```
<Note>
[Build extensions](/config/extensions/overview) allow you to hook into the build system and
customize the build process or the resulting bundle and container image (in the case of
deploying). You can use pre-built extensions or create your own.
</Note>
You'll also need to add `@trigger.dev/build` to your `package.json` file under `devDependencies` if you don't already have it there.
## Convert a document to PDF using LibreOffice and upload to R2
This task demonstrates how to use LibreOffice to convert a document (.doc or .docx) to PDF and upload the PDF to an R2 storage bucket.
### Key Features
- Fetches a document from a given URL
- Converts the document to PDF
- Uploads the PDF to R2 storage
### Task code
```ts trigger/libreoffice-pdf-convert.ts
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { task } from "@trigger.dev/sdk";
import libreoffice from "libreoffice-convert";
import { promisify } from "node:util";
import path from "path";
import fs from "fs";
const convert = promisify(libreoffice.convert);
// Initialize S3 client
const s3Client = new S3Client({
// How to authenticate to R2: https://developers.cloudflare.com/r2/api/s3/tokens/
region: "auto",
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "",
},
});
export const libreOfficePdfConvert = task({
id: "libreoffice-pdf-convert",
run: async (payload: { documentUrl: string }, { ctx }) => {
// Set LibreOffice path for production environment
if (ctx.environment.type !== "DEVELOPMENT") {
process.env.LIBREOFFICE_PATH = "/usr/bin/libreoffice";
}
try {
// Create temporary file paths
const inputPath = path.join(process.cwd(), `input_${Date.now()}.docx`);
const outputPath = path.join(process.cwd(), `output_${Date.now()}.pdf`);
// Download file from URL
const response = await fetch(payload.documentUrl);
const buffer = Buffer.from(await response.arrayBuffer());
fs.writeFileSync(inputPath, buffer);
const inputFile = fs.readFileSync(inputPath);
// Convert to PDF using LibreOffice
const pdfBuffer = await convert(inputFile, ".pdf", undefined);
fs.writeFileSync(outputPath, pdfBuffer);
// Upload to R2
const key = `converted-pdfs/output_${Date.now()}.pdf`;
await s3Client.send(
new PutObjectCommand({
Bucket: process.env.R2_BUCKET,
Key: key,
Body: fs.readFileSync(outputPath),
})
);
// Cleanup temporary files
fs.unlinkSync(inputPath);
fs.unlinkSync(outputPath);
return { pdfLocation: key };
} catch (error) {
console.error("Error converting PDF:", error);
throw error;
}
},
});
```
### Testing your task
To test this task, use this payload structure:
```json
{
"documentUrl": "<a-document-url>" // Replace <a-document-url> with the URL of the document you want to convert
}
```
<LocalDevelopment packages={"libreoffice"} />
+224
View File
@@ -0,0 +1,224 @@
---
title: "Lightpanda"
sidebarTitle: "Lightpanda"
description: "These examples demonstrate how to use Lightpanda with Trigger.dev."
tag: "v4"
---
import ScrapingWarning from "/snippets/web-scraping-warning.mdx";
## Overview
Lightpanda is a purpose-built browser for AI and automation workflows. It is 10x faster, uses 10x less RAM than Chrome headless.
Here are a few examples of how to use Lightpanda with Trigger.dev.
<ScrapingWarning />
## Limitations
- Lightpanda does not support the `puppeteer` screenshot feature.
## Using Lightpanda Cloud
### Prerequisites
- A [Lightpanda](https://lightpanda.io/) cloud token
### Get links from a website
In this task we use Lightpanda browser to get links from a provided URL. You will have to pass the URL as a payload when triggering the task.
Make sure to add `LIGHTPANDA_TOKEN` to your Trigger.dev dashboard on the Environment Variables page:
```bash
LIGHTPANDA_TOKEN="<your-token>"
```
```ts trigger/lightpanda-cloud-puppeteer.ts
import { logger, task } from "@trigger.dev/sdk";
import puppeteer from "puppeteer-core";
export const lightpandaCloudPuppeteer = task({
id: "lightpanda-cloud-puppeteer",
machine: {
preset: "micro",
},
run: async (payload: { url: string }, { ctx }) => {
logger.log("Lets get a page's links with Lightpanda!", { payload, ctx });
if (!payload.url) {
logger.warn("Please define the payload url");
throw new Error("payload.url is undefined");
}
const token = process.env.LIGHTPANDA_TOKEN;
if (!token) {
logger.warn("Please define the env variable LIGHTPANDA_TOKEN");
throw new Error("LIGHTPANDA_TOKEN is undefined");
}
// Connect to Lightpanda's cloud
const browser = await puppeteer.connect({
browserWSEndpoint: `wss://cloud.lightpanda.io/ws?browser=lightpanda&token=${token}`,
});
const context = await browser.createBrowserContext();
const page = await context.newPage();
// Dump all the links from the page.
await page.goto(payload.url);
const links = await page.evaluate(() => {
return Array.from(document.querySelectorAll("a")).map((row) => {
return row.getAttribute("href");
});
});
logger.info("Processing done, shutting down…");
await page.close();
await context.close();
await browser.disconnect();
logger.info("✅ Completed");
return {
links,
};
},
});
```
### Proxies
Proxies can be used with your browser via the proxy query string parameter. By default, the proxy used is "datacenter" which is a pool of shared datacenter IPs.
`datacenter` accepts an optional `country` query string parameter which is an [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country code.
```bash
# This example will use a German IP
wss://cloud.lightpanda.io/ws?proxy=datacenter&country=de&token=${token}
```
### Session
A session is alive until you close it or the connection is closed. The max duration of a session is 15 minutes.
## Using Lightpanda browser directly
### Prerequisites
- Setup the [Lightpanda build extension](/config/extensions/lightpanda)
### Get the HTML of a webpage
This task will dump the HTML of a provided URL using the Lightpanda browser binary. You will have to pass the URL as a payload when triggering the task.
```ts trigger/lightpanda-fetch.ts
import { logger, task } from "@trigger.dev/sdk";
import { execSync } from "node:child_process";
export const lightpandaFetch = task({
id: "lightpanda-fetch",
machine: {
preset: "micro",
},
run: async (payload: { url: string }, { ctx }) => {
logger.log("Lets get a page's content with Lightpanda!", { payload, ctx });
if (!payload.url) {
logger.warn("Please define the payload url");
throw new Error("payload.url is undefined");
}
const buffer = execSync(`lightpanda fetch --dump ${payload.url}`);
logger.info("✅ Completed");
return {
message: buffer.toString(),
};
},
});
```
### Lightpanda CDP with Puppeteer
This task initializes a Lightpanda CDP server and uses it with `puppeteer-core` to scrape a provided URL.
```ts trigger/lightpanda-cdp.ts
import { logger, task } from "@trigger.dev/sdk";
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import puppeteer from "puppeteer-core";
const spawnLightpanda = async (host: string, port: string) =>
new Promise<ChildProcessWithoutNullStreams>((resolve, reject) => {
const child = spawn("lightpanda", [
"serve",
"--host",
host,
"--port",
port,
"--log_level",
"info",
]);
child.on("spawn", async () => {
logger.info("Running Lightpanda's CDP server…", {
pid: child.pid,
});
await new Promise((resolve) => setTimeout(resolve, 250));
resolve(child);
});
child.on("error", (e) => reject(e));
});
export const lightpandaCDP = task({
id: "lightpanda-cdp",
machine: {
preset: "micro",
},
run: async (payload: { url: string }, { ctx }) => {
logger.log("Lets get a page's links with Lightpanda!", { payload, ctx });
if (!payload.url) {
logger.warn("Please define the payload url");
throw new Error("payload.url is undefined");
}
const host = process.env.LIGHTPANDA_CDP_HOST ?? "127.0.0.1";
const port = process.env.LIGHTPANDA_CDP_PORT ?? "9222";
// Launch Lightpanda's CDP server
const lpProcess = await spawnLightpanda(host, port);
const browser = await puppeteer.connect({
browserWSEndpoint: `ws://${host}:${port}`,
});
const context = await browser.createBrowserContext();
const page = await context.newPage();
// Dump all the links from the page.
await page.goto(payload.url);
const links = await page.evaluate(() => {
return Array.from(document.querySelectorAll("a")).map((row) => {
return row.getAttribute("href");
});
});
logger.info("Processing done");
logger.info("Shutting down…");
// Close Puppeteer instance
await browser.close();
// Stop Lightpanda's CDP Server
lpProcess.kill();
logger.info("✅ Completed");
return {
links,
};
},
});
```
@@ -0,0 +1,56 @@
---
title: "Call OpenAI with retrying"
sidebarTitle: "OpenAI with retrying"
description: "This example will show you how to call OpenAI with retrying using Trigger.dev."
---
## Overview
Sometimes OpenAI calls can take a long time to complete, or they can fail. This task will retry if the API call fails completely or if the response is empty.
## Task code
```ts trigger/openai.ts
import { task } from "@trigger.dev/sdk";
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
export const openaiTask = task({
id: "openai-task",
//specifying retry options overrides the defaults defined in your trigger.config file
retry: {
maxAttempts: 10,
factor: 1.8,
minTimeoutInMs: 500,
maxTimeoutInMs: 30_000,
randomize: false,
},
run: async (payload: { prompt: string }) => {
//if this fails, it will throw an error and retry
const chatCompletion = await openai.chat.completions.create({
messages: [{ role: "user", content: payload.prompt }],
model: "gpt-3.5-turbo",
});
if (chatCompletion.choices[0]?.message.content === undefined) {
//sometimes OpenAI returns an empty response, let's retry by throwing an error
throw new Error("OpenAI call failed");
}
return chatCompletion.choices[0].message.content;
},
});
```
## Testing your task
To test this task in the dashboard, you can use the following payload:
```json
{
"prompt": "What is the meaning of life?"
}
```
+102
View File
@@ -0,0 +1,102 @@
---
title: "Turn a PDF into an image using MuPDF"
sidebarTitle: "PDF to image"
description: "This example will show you how to turn a PDF into an image using MuPDF and Trigger.dev."
---
import LocalDevelopment from "/snippets/local-development-extensions.mdx";
## Overview
This example demonstrates how to use Trigger.dev to turn a PDF into a series of images using MuPDF and upload them to Cloudflare R2.
## Update your build configuration
To use this example, add these build settings below to your `trigger.config.ts` file. They ensure that the `mutool` and `curl` packages are installed when you deploy your task. You can learn more about this and see more build settings [here](/config/extensions/aptGet).
```ts trigger.config.ts
export default defineConfig({
project: "<project ref>",
// Your other config settings...
build: {
extensions: [aptGet({ packages: ["mupdf-tools", "curl"] })],
},
});
```
## Task code
```ts trigger/pdfToImage.ts
import { logger, task } from "@trigger.dev/sdk";
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { execSync } from "child_process";
import fs from "fs";
import path from "path";
// Initialize S3 client
const s3Client = new S3Client({
region: "auto",
endpoint: process.env.S3_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "",
},
});
export const pdfToImage = task({
id: "pdf-to-image",
run: async (payload: { pdfUrl: string; documentId: string }) => {
logger.log("Converting PDF to images", payload);
const pdfPath = `/tmp/${payload.documentId}.pdf`;
const outputDir = `/tmp/${payload.documentId}`;
// Download PDF and convert to images using MuPDF
execSync(`curl -s -o ${pdfPath} ${payload.pdfUrl}`);
fs.mkdirSync(outputDir, { recursive: true });
execSync(`mutool convert -o ${outputDir}/page-%d.png ${pdfPath}`);
// Upload images to R2
const uploadedUrls = [];
for (const file of fs.readdirSync(outputDir)) {
const s3Key = `images/${payload.documentId}/${file}`;
const uploadParams = {
Bucket: process.env.S3_BUCKET,
Key: s3Key,
Body: fs.readFileSync(path.join(outputDir, file)),
ContentType: "image/png",
};
logger.log("Uploading to R2", uploadParams);
await s3Client.send(new PutObjectCommand(uploadParams));
const s3Url = `https://${process.env.S3_BUCKET}.r2.cloudflarestorage.com/${s3Key}`;
uploadedUrls.push(s3Url);
logger.log("Image uploaded to R2", { url: s3Url });
}
// Clean up
fs.rmSync(outputDir, { recursive: true, force: true });
fs.unlinkSync(pdfPath);
logger.log("All images uploaded to R2", { urls: uploadedUrls });
return {
imageUrls: uploadedUrls,
};
},
});
```
## Testing your task
To test this task in the dashboard, you can use the following payload:
```json
{
"pdfUrl": "https://pdfobject.com/pdf/sample.pdf",
"documentId": "unique-document-id"
}
```
<LocalDevelopment packages={"mupdf-tools from MuPDF"} />
+218
View File
@@ -0,0 +1,218 @@
---
title: "Puppeteer"
sidebarTitle: "Puppeteer"
description: "These examples demonstrate how to use Puppeteer with Trigger.dev."
---
import LocalDevelopment from "/snippets/local-development-extensions.mdx";
import ScrapingWarning from "/snippets/web-scraping-warning.mdx";
## Prerequisites
- A project with [Trigger.dev initialized](/quick-start)
- [Puppeteer](https://pptr.dev/guides/installation) installed on your machine
## Overview
There are 3 example tasks to follow on this page:
1. [Basic example](/guides/examples/puppeteer#basic-example)
2. [Generate a PDF from a web page](/guides/examples/puppeteer#generate-a-pdf-from-a-web-page)
3. [Scrape content from a web page](/guides/examples/puppeteer#scrape-content-from-a-web-page)
<ScrapingWarning />
## Build configuration
To use all examples on this page, you'll first need to add these build settings to your `trigger.config.ts` file:
```ts trigger.config.ts
import { defineConfig } from "@trigger.dev/sdk";
import { puppeteer } from "@trigger.dev/build/extensions/puppeteer";
export default defineConfig({
project: "<project ref>",
// Your other config settings...
build: {
// This is required to use the Puppeteer library
extensions: [puppeteer()],
},
});
```
Learn more about the [trigger.config.ts](/config/config-file) file including setting default retry settings, customizing the build environment, and more.
## Set an environment variable
Set the following environment variable in your [Trigger.dev dashboard](/deploy-environment-variables) or [using the SDK](/deploy-environment-variables#in-your-code):
```bash
PUPPETEER_EXECUTABLE_PATH: "/usr/bin/google-chrome-stable",
```
## Basic example
### Overview
In this example we use [Puppeteer](https://pptr.dev/) to log out the title of a web page, in this case from the [Trigger.dev](https://trigger.dev) landing page.
### Task code
```ts trigger/puppeteer-basic-example.ts
import { logger, task } from "@trigger.dev/sdk";
import puppeteer from "puppeteer";
export const puppeteerTask = task({
id: "puppeteer-log-title",
run: async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto("https://trigger.dev");
const content = await page.title();
logger.info("Content", { content });
await browser.close();
},
});
```
### Testing your task
There's no payload required for this task so you can just click "Run test" from the Test page in the dashboard. Learn more about testing tasks [here](/run-tests).
## Generate a PDF from a web page
### Overview
In this example we use [Puppeteer](https://pptr.dev/) to generate a PDF from the [Trigger.dev](https://trigger.dev) landing page and upload it to [Cloudflare R2](https://developers.cloudflare.com/r2/).
### Task code
```ts trigger/puppeteer-generate-pdf.ts
import { logger, task } from "@trigger.dev/sdk";
import puppeteer from "puppeteer";
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
// Initialize S3 client
const s3Client = new S3Client({
region: "auto",
endpoint: process.env.S3_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "",
},
});
export const puppeteerWebpageToPDF = task({
id: "puppeteer-webpage-to-pdf",
run: async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
const response = await page.goto("https://trigger.dev");
const url = response?.url() ?? "No URL found";
// Generate PDF from the web page
const generatePdf = await page.pdf();
logger.info("PDF generated from URL", { url });
await browser.close();
// Upload to R2
const s3Key = `pdfs/test.pdf`;
const uploadParams = {
Bucket: process.env.S3_BUCKET,
Key: s3Key,
Body: generatePdf,
ContentType: "application/pdf",
};
logger.log("Uploading to R2 with params", uploadParams);
// Upload the PDF to R2 and return the URL.
await s3Client.send(new PutObjectCommand(uploadParams));
const s3Url = `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${s3Key}`;
logger.log("PDF uploaded to R2", { url: s3Url });
return { pdfUrl: s3Url };
},
});
```
### Testing your task
There's no payload required for this task so you can just click "Run test" from the Test page in the dashboard. Learn more about testing tasks [here](/run-tests).
## Scrape content from a web page
### Overview
In this example we use [Puppeteer](https://pptr.dev/) with a [BrowserBase](https://www.browserbase.com/) proxy to scrape the GitHub stars count from the [Trigger.dev](https://trigger.dev) landing page and log it out. See [this list](/guides/examples/puppeteer#proxying) for more proxying services we recommend.
<Warning>
When web scraping, you MUST use the technique below which uses a proxy with Puppeteer. Direct
scraping without using `browserWSEndpoint` is prohibited and will result in account suspension.
Screenshots are also prohibited when scraping.
</Warning>
### Task code
```ts trigger/scrape-website.ts
import { logger, task } from "@trigger.dev/sdk";
import puppeteer from "puppeteer-core";
export const puppeteerScrapeWithProxy = task({
id: "puppeteer-scrape-with-proxy",
run: async () => {
const browser = await puppeteer.connect({
browserWSEndpoint: `wss://connect.browserbase.com?apiKey=${process.env.BROWSERBASE_API_KEY}`,
});
const page = await browser.newPage();
try {
// Navigate to the target website
await page.goto("https://trigger.dev", { waitUntil: "networkidle0" });
// Scrape the GitHub stars count
const starCount = await page.evaluate(() => {
const starElement = document.querySelector(".github-star-count");
const text = starElement?.textContent ?? "0";
const numberText = text.replace(/[^0-9]/g, "");
return parseInt(numberText);
});
logger.info("GitHub star count", { starCount });
return { starCount };
} catch (error) {
logger.error("Error during scraping", {
error: error instanceof Error ? error.message : String(error),
});
throw error;
} finally {
await browser.close();
}
},
});
```
### Testing your task
There's no payload required for this task so you can just click "Run test" from the Test page in the dashboard. Learn more about testing tasks [here](/run-tests).
<LocalDevelopment packages={"the Puppeteer library."} />
## Proxying
If you're using Trigger.dev Cloud and Puppeteer or any other tool to scrape content from websites you don't own, you'll need to proxy your requests. **If you don't you'll risk getting our IP address blocked and we will ban you from our service. You must always have permission from the website owner to scrape their content.**
Here are a list of proxy services we recommend:
- [Browserbase](https://www.browserbase.com/)
- [Brightdata](https://brightdata.com/)
- [Browserless](https://browserless.io/)
- [Oxylabs](https://oxylabs.io/)
- [ScrapingBee](https://scrapingbee.com/)
- [Smartproxy](https://smartproxy.com/)
+360
View File
@@ -0,0 +1,360 @@
---
title: "Send emails using React Email"
sidebarTitle: "React Email"
description: "Learn how to send beautiful emails using React Email and Trigger.dev."
---
## Overview
This example demonstrates how to use Trigger.dev to send emails using [React Email](https://react.email/).
<Note>
This example uses [Resend](https://resend.com) as the email provider. You can use other email
providers like [Loops](https://loops.so) or [SendGrid](https://sendgrid.com) etc. Full list of
their integrations can be found [here](https://react.email/docs/introduction#integrations).
</Note>
## Task code
<Warning>
This email is built using React components. To use React components in your task, it must be a
.tsx file.
</Warning>
```tsx trigger/sendReactEmail.tsx
import { Body, Button, Container, Head, Heading, Html, Preview } from "@react-email/components";
import { logger, task } from "@trigger.dev/sdk";
import { Resend } from "resend";
// Initialize Resend client
const resend = new Resend(process.env.RESEND_API_KEY);
// React Email template component
const EmailTemplate = ({ name, message }: { name: string; message: string }) => (
<Html lang="en">
<Head />
<Preview>New message from {name}</Preview>
<Body style={{ fontFamily: "Arial, sans-serif", margin: "0", padding: "0" }}>
<Container style={{ padding: "20px", maxWidth: "600px" }}>
<Heading>Hello from Acme Inc.</Heading>
<p>Hi {name},</p>
<p>{message}</p>
<Button
href="https://trigger.dev"
style={{
backgroundColor: "#0070f3",
color: "white",
padding: "12px 20px",
borderRadius: "8px",
}}
>
Go to Acme Inc.
</Button>
</Container>
</Body>
</Html>
);
export const sendEmail = task({
id: "send-react-email",
run: async (payload: {
to: string;
name: string;
message: string;
subject: string;
from?: string;
}) => {
try {
logger.info("Sending email using React.email and Resend", {
to: payload.to,
});
// Send the email using Resend
const { data, error } = await resend.emails.send({
// The from address needs to be a verified email address you own
from: payload.from || "email@acmeinc.com", // Default from address
to: payload.to,
subject: payload.subject,
react: <EmailTemplate name={payload.name} message={payload.message} />,
});
if (error) {
logger.error("Failed to send email", { error });
throw new Error(`Failed to send email: ${error.message}`);
}
logger.info("Email sent successfully", { emailId: data?.id });
// Return the response from Resend
return {
id: data?.id,
status: "sent",
};
} catch (error) {
logger.error("Unexpected error sending email", { error });
throw error;
}
},
});
```
## The email
This example email should look like this:
![React Email](/images/react-email.png)
This is just a simple implementation, you can customize the email to be as complex as you want. Check out the [React email templates](https://react.email/templates) for more inspiration.
## Testing your task
To test this task in the [dashboard](https://cloud.trigger.dev), you can use the following payload:
```json
{
"to": "recipient@example.com",
"name": "Jane Doe",
"message": "Thank you for signing up for our service!",
"subject": "Welcome to Acme Inc."
}
```
## Deploying your task
Deploy the task to production using the Trigger.dev CLI `deploy` command.
## Using Cursor / AI to build your emails
In this video you can see how we use Cursor to build a welcome email.
We recommend using our [Cursor rules](https://trigger.dev/changelog/cursor-rules-writing-tasks/) to help you build your tasks and emails.
#### Video: creating a new email template using Cursor
<video
src="https://content.trigger.dev/trigger-welcome-email-cursor.mp4"
controls
muted
autoPlay
loop
/>
#### The generated email template
![Cursor](/images/react-email-welcome.png)
#### The generated code
```tsx emails/trigger-welcome-email.tsx
import {
Body,
Button,
Container,
Head,
Heading,
Hr,
Html,
Img,
Link,
Preview,
Section,
Text,
} from "@react-email/components";
const baseUrl = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "";
export interface TriggerWelcomeEmailProps {
name: string;
}
export const TriggerWelcomeEmail = ({ name }: TriggerWelcomeEmailProps) => (
<Html>
<Head />
<Preview>Welcome to Trigger.dev - Your background jobs platform!</Preview>
<Body style={main}>
<Container style={container}>
<Section style={box}>
<Img
src="https://trigger.dev/assets/triggerdev-lockup--light.svg"
width="150"
height="40"
alt="Trigger.dev"
/>
<Hr style={hr} />
<Heading>Welcome, {name}!</Heading>
<Text style={paragraph}>
Thanks for signing up for Trigger.dev! You're now ready to start creating powerful
background jobs and workflows.
</Text>
<Text style={paragraph}>
You can monitor your jobs, view runs, and manage your projects right from your
dashboard.
</Text>
<Button style={button} href="https://cloud.trigger.dev/dashboard">
View your Trigger.dev Dashboard
</Button>
<Hr style={hr} />
<Text style={paragraph}>
To help you get started, check out our{" "}
<Link style={anchor} href="https://trigger.dev/docs">
documentation
</Link>{" "}
and{" "}
<Link style={anchor} href="https://trigger.dev/docs/quickstart">
quickstart guide
</Link>
.
</Text>
<Text style={paragraph}>
You can create your first job using our SDK, set up integrations, and configure triggers
to automate your workflows. Take a look at our{" "}
<Link style={anchor} href="https://trigger.dev/docs/examples">
examples
</Link>{" "}
for inspiration.
</Text>
<Text style={paragraph}>
Join our{" "}
<Link style={anchor} href="https://discord.gg/kA47vcd8Qr">
Discord community
</Link>{" "}
to connect with other developers and get help when you need it.
</Text>
<Text style={paragraph}>
We're here to help you build amazing things. If you have any questions, check out our{" "}
<Link style={anchor} href="https://trigger.dev/docs">
documentation
</Link>{" "}
or reach out to us on Discord.
</Text>
<Text style={paragraph}>— The Trigger.dev team</Text>
<Hr style={hr} />
<Text style={footer}>Trigger.dev Inc.</Text>
</Section>
</Container>
</Body>
</Html>
);
export default TriggerWelcomeEmail;
const main = {
backgroundColor: "#0E0C15",
fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
};
const container = {
backgroundColor: "#1D1B27",
margin: "0 auto",
padding: "20px 0 48px",
marginBottom: "64px",
};
const box = {
padding: "0 48px",
};
const hr = {
borderColor: "#2D2B3B",
margin: "20px 0",
};
const paragraph = {
color: "#E1E1E3",
fontSize: "16px",
lineHeight: "24px",
textAlign: "left" as const,
};
const anchor = {
color: "#A78BFA",
};
const button = {
backgroundColor: "#7C3AED",
borderRadius: "6px",
color: "#fff",
fontSize: "16px",
fontWeight: "bold",
textDecoration: "none",
textAlign: "center" as const,
display: "block",
width: "100%",
padding: "12px",
};
const footer = {
color: "#9CA3AF",
fontSize: "12px",
lineHeight: "16px",
};
```
And then to trigger the email, you can use the following task:
```tsx trigger/triggerWelcomeEmail.tsx
import { logger, task } from "@trigger.dev/sdk";
import { Resend } from "resend";
import TriggerWelcomeEmail from "emails/trigger-welcome-email";
// Initialize Resend client
const resend = new Resend(process.env.RESEND_API_KEY);
export const sendEmail = task({
id: "trigger-welcome-email",
run: async (payload: { to: string; name: string; subject: string; from?: string }) => {
try {
to: payload.to,
});
const { data, error } = await resend.emails.send({
// The from address needs to be a verified email address
from: payload.from || "email@acmeinc.com", // Default from address
to: payload.to,
subject: payload.subject,
react: <TriggerWelcomeEmail name={payload.name} />,
});
if (error) {
logger.error("Failed to send email", { error });
throw new Error(`Failed to send email: ${error.message}`);
}
logger.info("Email sent successfully", { emailId: data?.id });
return {
id: data?.id,
status: "sent",
};
} catch (error) {
logger.error("Unexpected error sending email", { error });
throw error;
}
},
});
```
## Troubleshooting
If you see this error when using `react-email` packages:
```
reactDOMServer.renderToPipeableStream is not a function
```
See our [common problems guide](/troubleshooting#reactdomserver-rendertopipeablestream-is-not-a-function-when-using-react-email) for more information.
## Learn more
### React Email docs
Check out the [React Email docs](https://react.email/docs) and learn how to set up and use React Email, including how to preview your emails locally.
<CardGroup cols={2}>
<Card title="Components" icon="puzzle-piece" href="https://react.email/components">
Pre-built components you can copy and paste into your emails.
</Card>
<Card title="Templates" icon="rectangle-list" href="https://react.email/templates">
Extensive pre-built templates ready to use.
</Card>
</CardGroup>
+85
View File
@@ -0,0 +1,85 @@
---
title: "Generate a PDF using react-pdf and save it to R2"
sidebarTitle: "React to PDF"
description: "This example will show you how to generate a PDF using Trigger.dev."
---
## Overview
This example demonstrates how to use Trigger.dev to generate a PDF using [react-pdf](https://react-pdf.org/) and save it to Cloudflare R2.
## Task code
<Info> This example must be a .tsx file to use React components.</Info>
```ts trigger/generateResumePDF.tsx
import { logger, task } from "@trigger.dev/sdk";
import { renderToBuffer, Document, Page, Text, View } from "@react-pdf/renderer";
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
// Initialize R2 client
const r2Client = new S3Client({
// How to authenticate to R2: https://developers.cloudflare.com/r2/api/s3/tokens/
region: "auto",
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "",
},
});
export const generateResumePDF = task({
id: "generate-resume-pdf",
run: async (payload: { text: string }) => {
// Log the payload
logger.log("Generating PDF resume", payload);
// Render the ResumeDocument component to a PDF buffer
const pdfBuffer = await renderToBuffer(
<Document>
<Page size="A4">
<View>
<Text>{payload.text}</Text>
</View>
</Page>
</Document>
);
// Generate a unique filename based on the text and current timestamp
const filename = `${payload.text.replace(/\s+/g, "-").toLowerCase()}-${Date.now()}.pdf`;
// Set the R2 key for the PDF file
const r2Key = `resumes/${filename}`;
// Set the upload parameters for R2
const uploadParams = {
Bucket: process.env.R2_BUCKET,
Key: r2Key,
Body: pdfBuffer,
ContentType: "application/pdf",
};
// Log the upload parameters
logger.log("Uploading to R2 with params", uploadParams);
// Upload the PDF to R2
await r2Client.send(new PutObjectCommand(uploadParams));
// Return the Bucket and R2 key for the uploaded PDF
return {
Bucket: process.env.R2_BUCKET,
Key: r2Key,
};
},
});
```
## Testing your task
To test this task in the dashboard, you can use the following payload:
```json
{
"text": "Hello, world!"
}
```
@@ -0,0 +1,109 @@
---
title: "Image-to-image generation using Replicate and nano-banana"
sidebarTitle: "Replicate image generation"
description: "Learn how to generate images from source image URLs using Replicate and Trigger.dev."
---
## Overview
This example demonstrates how to use Trigger.dev to generate images from source image URLs using [Replicate](https://replicate.com/), the [nano-banana](https://replicate.com/google/nano-banana) model.
## Task code
```tsx trigger/generateImage.tsx
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { task, wait } from "@trigger.dev/sdk";
import Replicate, { Prediction } from "replicate";
// Initialize clients
const replicate = new Replicate({
auth: process.env.REPLICATE_API_TOKEN,
});
const s3Client = new S3Client({
region: "auto",
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "",
},
});
const model = "google/nano-banana";
export const generateImageAndUploadToR2 = task({
id: "generate-image-and-upload-to-r2",
run: async (payload: { prompt: string; imageUrl: string }) => {
const { prompt, imageUrl } = payload;
const token = await wait.createToken({
timeout: "10m",
});
// Use Flux with structured prompt
const output = await replicate.predictions.create({
model: model,
input: { prompt, image_input: [imageUrl] },
// pass the provided URL to Replicate's webhook, so they can "callback"
webhook: token.url,
webhook_events_filter: ["completed"],
});
const result = await wait.forToken<Prediction>(token).unwrap();
// unwrap() throws a timeout error or returns the result 👆
if (!result.ok) {
throw new Error("Failed to create prediction");
}
const generatedImageUrl = result.output.output;
const image = await fetch(generatedImageUrl);
const imageBuffer = Buffer.from(await image.arrayBuffer());
const base64Image = Buffer.from(imageBuffer).toString("base64");
const timestamp = Date.now();
const filename = `generated-${timestamp}.png`;
// Generate unique key for R2
const sanitizedFileName = filename.replace(/[^a-zA-Z0-9.-]/g, "_");
const r2Key = `uploaded-images/${timestamp}-${sanitizedFileName}`;
const uploadParams = {
Bucket: process.env.R2_BUCKET,
Key: r2Key,
Body: imageBuffer,
ContentType: "image/png",
// Add cache control for better performance
CacheControl: "public, max-age=31536000", // 1 year
};
const uploadResult = await s3Client.send(new PutObjectCommand(uploadParams));
// Construct the public URL using the R2_PUBLIC_URL env var
const publicUrl = `${process.env.R2_PUBLIC_URL}/${r2Key}`;
return {
success: true,
publicUrl,
originalPrompt: prompt,
sourceImageUrl: imageUrl,
};
},
});
```
## Environment variables
You will need to set the following environment variables:
```
TRIGGER_SECRET_KEY=<your-trigger-secret-key>
REPLICATE_API_TOKEN=<your-replicate-api-token>
R2_ENDPOINT=<your-r2-endpoint>
R2_ACCESS_KEY_ID=<your-r2-access-key-id>
R2_SECRET_ACCESS_KEY=<your-r2-secret-access-key>
R2_BUCKET=<your-r2-bucket>
R2_PUBLIC_URL=<your-r2-public-url>
```
@@ -0,0 +1,83 @@
---
title: "Send a sequence of emails using Resend"
sidebarTitle: "Resend email sequence"
description: "This example will show you how to send a sequence of emails over several days using Resend with Trigger.dev."
---
## Overview
Each email is wrapped in retry.onThrow. This will retry the block of code if an error is thrown. This is useful when you dont want to retry the whole task, but just a part of it. The entire task will use the default retrying, so can also retry.
Additionally this task uses wait.for to wait for a certain amount of time before sending the next email. During the waiting time, the task will be paused and will not consume any resources.
## Task code
```ts trigger/email-sequence.ts
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_ASP_KEY);
export const emailSequence = task({
id: "email-sequence",
run: async (payload: { userId: string; email: string; name: string }) => {
console.log(`Start email sequence for user ${payload.userId}`, payload);
// Send the first email immediately
const firstEmailResult = await retry.onThrow(
async ({ attempt }) => {
const { data, error } = await resend.emails.send({
from: "hello@trigger.dev",
to: payload.email,
subject: "Welcome to Trigger.dev",
html: `<p>Hello ${payload.name},</p><p>Welcome to Trigger.dev</p>`,
});
if (error) {
// Throwing an error will trigger a retry of this block
throw error;
}
return data;
},
{ maxAttempts: 3 }
);
// Then wait 3 days
await wait.for({ days: 3 });
// Send the second email
const secondEmailResult = await retry.onThrow(
async ({ attempt }) => {
const { data, error } = await resend.emails.send({
from: "hello@trigger.dev",
to: payload.email,
subject: "Some tips for you",
html: `<p>Hello ${payload.name},</p><p>Here are some tips for you…</p>`,
});
if (error) {
// Throwing an error will trigger a retry of this block
throw error;
}
return data;
},
{ maxAttempts: 3 }
);
//etc...
},
});
```
## Testing your task
To test this task in the dashboard, you can use the following payload:
```json
{
"userId": "123",
"email": "<your-test-email>", // Replace with your test email
"name": "Alice Testington"
}
```
+133
View File
@@ -0,0 +1,133 @@
---
title: "Generate OG Images using Satori"
sidebarTitle: "Satori OG Images"
description: "Learn how to generate dynamic Open Graph images using Satori and Trigger.dev."
---
## Overview
This example demonstrates how to use Trigger.dev to generate dynamic Open Graph (OG) images using Vercel's [Satori](https://github.com/vercel/satori). The task takes a title and image URL as input and generates a beautiful OG image with text overlay.
This can be customized and extended however you like, full list of options can be found [here](https://github.com/vercel/satori).
## Task code
```tsx trigger/generateOgImage.ts
import { schemaTask } from "@trigger.dev/sdk";
import { z } from "zod";
import satori from "satori";
import sharp from "sharp";
import { join } from "path";
import fs from "fs/promises";
export const generateOgImage = schemaTask({
id: "generate-og-image",
schema: z.object({
width: z.number().optional(),
height: z.number().optional(),
title: z.string(),
imageUrl: z.string().url(),
}),
run: async (payload) => {
// Load font
const fontResponse = await fetch(
"https://github.com/googlefonts/roboto/raw/main/src/hinted/Roboto-Regular.ttf"
).then((res) => res.arrayBuffer());
// Fetch and convert image to base64
const imageResponse = await fetch(payload.imageUrl);
const imageBuffer = await imageResponse.arrayBuffer();
const imageBase64 = `data:${
imageResponse.headers.get("content-type") || "image/jpeg"
};base64,${Buffer.from(imageBuffer).toString("base64")}`;
const markup = (
<div
style={{
width: payload.width ?? 1200,
height: payload.height ?? 630,
display: "flex",
backgroundColor: "#121317",
position: "relative",
fontFamily: "Roboto",
}}
>
<img
src={imageBase64}
width={payload.width ?? 1200}
height={payload.height ?? 630}
style={{
objectFit: "cover",
}}
/>
<h1
style={{
fontSize: "60px",
fontWeight: "bold",
color: "#fff",
margin: 0,
position: "absolute",
top: "50%",
transform: "translateY(-50%)",
left: "48px",
maxWidth: "60%",
textShadow: "0 2px 4px rgba(0,0,0,0.5)",
}}
>
{payload.title}
</h1>
</div>
);
const svg = await satori(markup, {
width: payload.width ?? 1200,
height: payload.height ?? 630,
fonts: [
{
name: "Roboto",
data: fontResponse,
weight: 400,
style: "normal",
},
],
});
const fileName = `og-${Date.now()}.jpg`;
const tempDir = join(process.cwd(), "tmp");
await fs.mkdir(tempDir, { recursive: true });
const outputPath = join(tempDir, fileName);
await sharp(Buffer.from(svg))
.jpeg({
quality: 90,
mozjpeg: true,
})
.toFile(outputPath);
return {
filePath: outputPath,
width: payload.width,
height: payload.height,
};
},
});
```
## Image example
This image was generated using the above task.
![OG Image](/images/react-satori-og.jpg)
## Testing your task
To test this task in the [dashboard](https://cloud.trigger.dev), you can use the following payload:
```json
{
"title": "My Awesome OG image",
"imageUrl": "<your-image-url>",
"width": 1200, // optional, defaults to 1200
"height": 630 // optional, defaults to 630
}
```
+252
View File
@@ -0,0 +1,252 @@
---
title: "Scrape the top 3 articles from Hacker News and email yourself a summary every weekday"
sidebarTitle: "Browserbase & Puppeteer"
description: "This example demonstrates how to scrape the top 3 articles from Hacker News using BrowserBase and Puppeteer, summarize them with ChatGPT and send a nicely formatted email summary to yourself every weekday using Resend."
---
import LocalDevelopment from "/snippets/local-development-extensions.mdx";
import ScrapingWarning from "/snippets/web-scraping-warning.mdx";
<iframe
width="100%"
height="315"
src="https://www.youtube.com/embed/6azvzrZITKY?si=muKtsBiS9TJGGKWg"
title="YouTube video player"
allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerPolicy="strict-origin-when-cross-origin"
allowFullScreen
/>
## Overview
In this example we'll be using a number of different tools and features to:
1. Scrape the content of the top 3 articles from Hacker News
2. Summarize each article
3. Email the summaries to yourself
And we'll be using the following tools and features:
- [Schedules](/tasks/scheduled) to run the task every weekday at 9 AM
- [Batch Triggering](/triggering#yourtask-batchtriggerandwait) to run separate child tasks for each article while the parent task waits for them all to complete
- [idempotencyKey](/triggering#idempotencykey) to prevent tasks being triggered multiple times
- [BrowserBase](https://browserbase.com/) to proxy the scraping of the Hacker News articles
- [Puppeteer](https://pptr.dev/) to scrape the articles linked from Hacker News
- [OpenAI](https://platform.openai.com/docs/overview) to summarize the articles
- [Resend](https://resend.com/) to send a nicely formatted email summary
<ScrapingWarning />
## Prerequisites
- A project with [Trigger.dev initialized](/quick-start)
- [Puppeteer](https://pptr.dev/guides/installation) installed on your machine
- A [BrowserBase](https://browserbase.com/) account
- An [OpenAI](https://platform.openai.com/docs/overview) account
- A [Resend](https://resend.com/) account
## Build configuration
First up, add these build settings to your `trigger.config.ts` file:
```tsx trigger.config.ts
import { defineConfig } from "@trigger.dev/sdk";
import { puppeteer } from "@trigger.dev/build/extensions/puppeteer";
export default defineConfig({
project: "<project ref>",
// Your other config settings...
build: {
// This is required to use the Puppeteer library
extensions: [puppeteer()],
},
});
```
Learn more about the [trigger.config.ts](/config/config-file) file including setting default retry settings, customizing the build environment, and more.
### Environment variables
Set the following environment variable in your local `.env` file to run this task locally. And before deploying your task, set them in the [Trigger.dev dashboard](/deploy-environment-variables) or [using the SDK](/deploy-environment-variables#in-your-code):
```bash
BROWSERBASE_API_KEY: "<your BrowserBase API key>"
OPENAI_API_KEY: "<your OpenAI API key>"
RESEND_API_KEY: "<your Resend API key>"
```
### Task code
```ts trigger/scrape-hacker-news.ts
import { render } from "@react-email/render";
import { logger, schedules, task, wait } from "@trigger.dev/sdk";
import { OpenAI } from "openai";
import puppeteer from "puppeteer-core";
import { Resend } from "resend";
import { HNSummaryEmail } from "./summarize-hn-email";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const resend = new Resend(process.env.RESEND_API_KEY);
// Parent task (scheduled to run 9AM every weekday)
export const summarizeHackerNews = schedules.task({
id: "summarize-hacker-news",
cron: {
pattern: "0 9 * * 1-5",
timezone: "Europe/London",
}, // Run at 9 AM, Monday to Friday
run: async () => {
// Connect to BrowserBase to proxy the scraping of the Hacker News articles
const browser = await puppeteer.connect({
browserWSEndpoint: `wss://connect.browserbase.com?apiKey=${process.env.BROWSERBASE_API_KEY}`,
});
logger.info("Connected to Browserbase");
const page = await browser.newPage();
// Navigate to Hacker News and scrape top 3 articles
await page.goto("https://news.ycombinator.com/news", {
waitUntil: "networkidle0",
});
logger.info("Navigated to Hacker News");
const articles = await page.evaluate(() => {
const items = document.querySelectorAll(".athing");
return Array.from(items)
.slice(0, 3)
.map((item) => {
const titleElement = item.querySelector(".titleline > a");
const link = titleElement?.getAttribute("href");
const title = titleElement?.textContent;
return { title, link };
});
});
logger.info("Scraped top 3 articles", { articles });
await browser.close();
await wait.for({ seconds: 5 });
// Use batchTriggerAndWait to process articles
const summaries = await scrapeAndSummarizeArticle
.batchTriggerAndWait(
articles.map((article) => ({
payload: { title: article.title!, link: article.link! },
}))
)
.then((batch) => batch.runs.filter((run) => run.ok).map((run) => run.output));
// Send email using Resend
await resend.emails.send({
from: "Hacker News Summary <hi@demo.tgr.dev>",
to: ["james@trigger.dev"],
subject: "Your morning HN summary",
html: render(<HNSummaryEmail articles={summaries} />),
});
logger.info("Email sent successfully");
},
});
// Child task for scraping and summarizing individual articles
export const scrapeAndSummarizeArticle = task({
id: "scrape-and-summarize-articles",
retry: {
maxAttempts: 3,
minTimeoutInMs: 5000,
maxTimeoutInMs: 10000,
factor: 2,
randomize: true,
},
run: async ({ title, link }: { title: string; link: string }) => {
logger.info(`Summarizing ${title}`);
const browser = await puppeteer.connect({
browserWSEndpoint: `wss://connect.browserbase.com?apiKey=${process.env.BROWSERBASE_API_KEY}`,
});
const page = await browser.newPage();
// Prevent all assets from loading, images, stylesheets etc
await page.setRequestInterception(true);
page.on("request", (request) => {
if (["script", "stylesheet", "image", "media", "font"].includes(request.resourceType())) {
request.abort();
} else {
request.continue();
}
});
await page.goto(link, { waitUntil: "networkidle0" });
logger.info(`Navigated to article: ${title}`);
// Extract the main content of the article
const content = await page.evaluate(() => {
const articleElement = document.querySelector("article") || document.body;
return articleElement.innerText.trim().slice(0, 1500); // Limit to 1500 characters
});
await browser.close();
logger.info(`Extracted content for article: ${title}`, { content });
// Summarize the content using ChatGPT
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{
role: "user",
content: `Summarize this article in 2-3 concise sentences:\n\n${content}`,
},
],
});
logger.info(`Generated summary for article: ${title}`);
return {
title,
link,
summary: response.choices[0].message.content,
};
},
});
```
## Create your email template using React Email
To prevent the main example from becoming too cluttered, we'll create a separate file for our email template. It's formatted using [React Email](https://react.email/docs/introduction) components so you'll need to install the package to use it.
Notice how this file is imported into the main task code and passed to Resend to send the email.
```tsx summarize-hn-email.tsx
import { Html, Head, Body, Container, Section, Heading, Text, Link } from "@react-email/components";
interface Article {
title: string;
link: string;
summary: string | null;
}
export const HNSummaryEmail: React.FC<{ articles: Article[] }> = ({ articles }) => (
<Html>
<Head />
<Body style={{ fontFamily: "Arial, sans-serif", padding: "20px" }}>
<Container>
<Heading as="h1">Your Morning HN Summary</Heading>
{articles.map((article, index) => (
<Section key={index} style={{ marginBottom: "20px" }}>
<Heading as="h3">
<Link href={article.link}>{article.title}</Link>
</Heading>
<Text>{article.summary || "No summary available"}</Text>
</Section>
))}
</Container>
</Body>
</Html>
);
```
<LocalDevelopment packages={"the Puppeteer library"} />
## Testing your task
To test this task in the dashboard, use the Test page and set the schedule date to "Now" to ensure the task triggers immediately. Then click "Run test" and wait for the task to complete.
@@ -0,0 +1,176 @@
---
title: "Track errors with Sentry"
sidebarTitle: "Sentry error tracking"
description: "This example demonstrates how to track errors with Sentry using Trigger.dev."
---
## Overview
Automatically send errors and source maps to your Sentry project from your Trigger.dev tasks. Sending source maps to Sentry allows for more detailed stack traces when errors occur, as Sentry can map the minified code back to the original source code.
## Prerequisites
- A [Sentry](https://sentry.io) account and project
- A [Trigger.dev](https://trigger.dev) account and project
## Setup
This setup involves two files:
1. **`trigger.config.ts`** - Configures the build to upload source maps to Sentry during deployment
2. **`trigger/init.ts`** - Initializes Sentry and registers the error tracking hook at runtime
<Note>
You will need to set the `SENTRY_AUTH_TOKEN` and `SENTRY_DSN` environment variables. You can find
the `SENTRY_AUTH_TOKEN` in your Sentry dashboard, in settings -> developer settings -> auth tokens
and the `SENTRY_DSN` in your Sentry dashboard, in settings -> projects -> your project -> client
keys (DSN). Add these to your `.env` file, and in your [Trigger.dev
dashboard](https://cloud.trigger.dev), under environment variables in your project's sidebar.
</Note>
### Build configuration
Add this build configuration to your `trigger.config.ts` file. This uses the Sentry esbuild plugin to upload source maps every time you deploy your project.
```ts trigger.config.ts
import { defineConfig } from "@trigger.dev/sdk";
import { esbuildPlugin } from "@trigger.dev/build/extensions";
import { sentryEsbuildPlugin } from "@sentry/esbuild-plugin";
export default defineConfig({
project: "<project ref>",
// Your other config settings...
build: {
extensions: [
esbuildPlugin(
sentryEsbuildPlugin({
org: "<your-sentry-org>",
project: "<your-sentry-project>",
// Find this auth token in settings -> developer settings -> auth tokens
authToken: process.env.SENTRY_AUTH_TOKEN,
}),
{ placement: "last", target: "deploy" }
),
],
},
});
```
<Note>
[Build extensions](/config/extensions/overview) allow you to hook into the build system and
customize the build process or the resulting bundle and container image (in the case of
deploying). You can use pre-built extensions or create your own.
</Note>
### Bun runtime
If you are using the Bun runtime, esbuild's bundling of `@sentry/node`'s CJS entry can cause a runtime error in the local dev environment. Add the following extension to mark `@sentry/node` as external in dev only:
```ts trigger.config.ts
import { defineConfig } from "@trigger.dev/sdk";
import { esbuildPlugin } from "@trigger.dev/build/extensions";
import { sentryEsbuildPlugin } from "@sentry/esbuild-plugin";
export default defineConfig({
project: "<project ref>",
runtime: "bun",
build: {
extensions: [
{
name: "sentry-external-dev",
externalsForTarget: (target) => (target === "dev" ? ["@sentry/node"] : []),
},
esbuildPlugin(
sentryEsbuildPlugin({
org: "<your-sentry-org>",
project: "<your-sentry-project>",
authToken: process.env.SENTRY_AUTH_TOKEN,
}),
{ placement: "last", target: "deploy" }
),
],
},
});
```
This lets Bun resolve `@sentry/node` directly from `node_modules` during dev, while still bundling it normally for deployment.
### Runtime initialization
Create a `trigger/init.ts` file to initialize Sentry and register the global `onFailure` hook. This file is automatically loaded when your tasks execute.
```ts trigger/init.ts
import { tasks } from "@trigger.dev/sdk";
import * as Sentry from "@sentry/node";
// Initialize Sentry
Sentry.init({
defaultIntegrations: false,
// The Data Source Name (DSN) is a unique identifier for your Sentry project.
dsn: process.env.SENTRY_DSN,
// Update this to match the environment you want to track errors for
environment: process.env.NODE_ENV === "production" ? "production" : "development",
});
// Register a global onFailure hook to capture errors
tasks.onFailure(({ payload, error, ctx }) => {
Sentry.captureException(error, {
extra: {
payload,
ctx,
},
});
});
```
<Note>
Learn more about [global lifecycle hooks](/tasks/overview#global-lifecycle-hooks) and the
[`init.ts` file](/tasks/overview#init-ts).
</Note>
## Testing that errors are being sent to Sentry
To test that errors are being sent to Sentry, you need to create a task that will fail.
This task takes no payload, and will throw an error.
```ts trigger/sentry-error-test.ts
import { task } from "@trigger.dev/sdk";
export const sentryErrorTest = task({
id: "sentry-error-test",
retry: {
// Only retry once
maxAttempts: 1,
},
run: async () => {
const error = new Error("This is a custom error that Sentry will capture");
error.cause = { additionalContext: "This is additional context" };
throw error;
},
});
```
After creating the task, deploy your project.
<CodeGroup>
```bash npm
npx trigger.dev@latest deploy
```
```bash pnpm
pnpm dlx trigger.dev@latest deploy
```
```bash yarn
yarn dlx trigger.dev@latest deploy
```
</CodeGroup>
Once deployed, navigate to the `test` page in the sidebar of your [Trigger.dev dashboard](https://cloud.trigger.dev), click on your `prod` environment, and select the `sentryErrorTest` task.
Run a test task with an empty payload by clicking the `Run test` button.
Your run should then fail, and if everything is set up correctly, you will see an error in the Sentry project dashboard shortly after.
@@ -0,0 +1,130 @@
---
title: "Process images using Sharp"
sidebarTitle: "Sharp image processing"
description: "This example demonstrates how to process images using the Sharp library with Trigger.dev."
---
import LocalDevelopment from "/snippets/local-development-extensions.mdx";
## Overview
This task processes and watermarks an image using the Sharp library, and then uploads it to R2 storage.
## Prerequisites
- A project with [Trigger.dev initialized](/quick-start)
- The [Sharp](https://sharp.pixelplumbing.com/install) library installed on your machine
- An R2-compatible object storage service, such as [Cloudflare R2](https://developers.cloudflare.com/r2)
## Adding the build configuration
To use this example, you'll first need to add these build settings to your `trigger.config.ts` file:
```ts trigger.config.ts
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
project: "<project ref>",
// Your other config settings...
build: {
// This is required to use the Sharp library
external: ["sharp"],
},
});
```
<Note>
Any packages that install or build a native binary should be added to external, as native binaries
cannot be bundled.
</Note>
## Key features
- Resizes a JPEG image to 800x800 pixels
- Adds a watermark to the image, positioned in the bottom-right corner, using a PNG image
- Uploads the processed image to R2 storage
## Task code
```ts trigger/sharp-image-processing.ts
import { S3Client } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
import { logger, task } from "@trigger.dev/sdk";
import fs from "fs/promises";
import os from "os";
import path from "path";
import sharp from "sharp";
// Initialize R2 client using your R2 account details
const r2Client = new S3Client({
region: "auto",
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "",
},
});
export const sharpProcessImage = task({
id: "sharp-process-image",
retry: { maxAttempts: 1 },
run: async (payload: { imageUrl: string; watermarkUrl: string }) => {
const { imageUrl, watermarkUrl } = payload;
const outputPath = path.join(os.tmpdir(), `output_${Date.now()}.jpg`);
const [imageResponse, watermarkResponse] = await Promise.all([
fetch(imageUrl),
fetch(watermarkUrl),
]);
const imageBuffer = await imageResponse.arrayBuffer();
const watermarkBuffer = await watermarkResponse.arrayBuffer();
await sharp(Buffer.from(imageBuffer))
.resize(800, 800) // Resize the image to 800x800px
.composite([
{
input: Buffer.from(watermarkBuffer),
gravity: "southeast", // Position the watermark in the bottom-right corner
},
])
.jpeg() // Convert to jpeg
.toBuffer() // Convert to buffer
.then(async (outputBuffer) => {
await fs.writeFile(outputPath, outputBuffer); // Write the buffer to file
const r2Key = `processed-images/${path.basename(outputPath)}`;
const uploadParams = {
Bucket: process.env.R2_BUCKET,
Key: r2Key,
Body: await fs.readFile(outputPath),
};
const upload = new Upload({
client: r2Client,
params: uploadParams,
});
await upload.done();
logger.log("Image uploaded to R2 storage.", {
path: `/${process.env.R2_BUCKET}/${r2Key}`,
});
await fs.unlink(outputPath); // Clean up the temporary file
return { r2Key };
});
},
});
```
## Testing your task
To test this task in the dashboard, you can use the following payload:
```json
{
"imageUrl": "<an-image-url.jpg>", // Replace with a URL to a JPEG image
"watermarkUrl": "<an-image-url.png>" // Replace with a URL to a PNG watermark image
}
```
<LocalDevelopment packages={"the Sharp image processing library"} />
+151
View File
@@ -0,0 +1,151 @@
---
title: "Trigger a task from Stripe webhook events"
sidebarTitle: "Stripe webhooks"
description: "This example demonstrates how to handle Stripe webhook events using Trigger.dev."
---
## Overview
This example shows how to set up a webhook handler in your existing app for incoming Stripe events. The handler triggers a task when a `checkout.session.completed` event is received. This is easily customisable to handle other Stripe events.
## Key features
- Shows how to create a Stripe webhook handler in your app
- Triggers a task from your backend when a `checkout.session.completed` event is received
## Environment variables
You'll need to configure the following environment variables for this example to work:
- `STRIPE_WEBHOOK_SECRET` The secret key used to verify the Stripe webhook signature.
- `TRIGGER_API_URL` Your Trigger.dev API url: `https://api.trigger.dev`
- `TRIGGER_SECRET_KEY` Your Trigger.dev secret key
## Setting up the Stripe webhook handler
First you'll need to create a [Stripe webhook](https://stripe.com/docs/webhooks) handler route that listens for POST requests and verifies the Stripe signature.
Here are examples of how you can set up a handler using different frameworks:
<CodeGroup>
```ts Next.js
// app/api/stripe-webhook/route.ts
import { NextResponse } from "next/server";
import { tasks } from "@trigger.dev/sdk";
import Stripe from "stripe";
import type { stripeCheckoutCompleted } from "@/trigger/stripe-checkout-completed";
// 👆 **type-only** import
export async function POST(request: Request) {
const signature = request.headers.get("stripe-signature");
const payload = await request.text();
if (!signature || !payload) {
return NextResponse.json(
{ error: "Invalid Stripe payload/signature" },
{
status: 400,
}
);
}
const event = Stripe.webhooks.constructEvent(
payload,
signature,
process.env.STRIPE_WEBHOOK_SECRET as string
);
// Perform the check based on the event type
switch (event.type) {
case "checkout.session.completed": {
// Trigger the task only if the event type is "checkout.session.completed"
const { id } = await tasks.trigger<typeof stripeCheckoutCompleted>(
"stripe-checkout-completed",
event.data.object
);
return NextResponse.json({ runId: id });
}
default: {
// Return a response indicating that the event is not handled
return NextResponse.json(
{ message: "Event not handled" },
{
status: 200,
}
);
}
}
}
```
```ts Remix
// app/webhooks.stripe.ts
import { type ActionFunctionArgs, json } from "@remix-run/node";
import type { stripeCheckoutCompleted } from "src/trigger/stripe-webhook";
// 👆 **type-only** import
import { tasks } from "@trigger.dev/sdk";
import Stripe from "stripe";
export async function action({ request }: ActionFunctionArgs) {
// Validate the Stripe webhook payload
const signature = request.headers.get("stripe-signature");
const payload = await request.text();
if (!signature || !payload) {
return json({ error: "Invalid Stripe payload/signature" }, { status: 400 });
}
const event = Stripe.webhooks.constructEvent(
payload,
signature,
process.env.STRIPE_WEBHOOK_SECRET as string
);
// Perform the check based on the event type
switch (event.type) {
case "checkout.session.completed": {
// Trigger the task only if the event type is "checkout.session.completed"
const { id } = await tasks.trigger<typeof stripeCheckoutCompleted>(
"stripe-checkout-completed",
event.data.object
);
return json({ runId: id });
}
default: {
// Return a response indicating that the event is not handled
return json({ message: "Event not handled" }, { status: 200 });
}
}
}
```
</CodeGroup>
## Task code
This task is triggered when a `checkout.session.completed` event is received from Stripe.
```ts trigger/stripe-checkout-completed.ts
import { task } from "@trigger.dev/sdk";
import type stripe from "stripe";
export const stripeCheckoutCompleted = task({
id: "stripe-checkout-completed",
run: async (payload: stripe.Checkout.Session) => {
// Add your custom logic for handling the checkout.session.completed event here
},
});
```
## Testing your task locally
To test everything is working you can use the Stripe CLI to send test events to your endpoint:
1. Install the [Stripe CLI](https://stripe.com/docs/stripe-cli#install), and login
2. Follow the instructions to [test your handler](https://docs.stripe.com/webhooks#test-webhook). This will include a temporary `STRIPE_WEBHOOK_SECRET` that you can use for testing.
3. When triggering the event, use the `checkout.session.completed` event type. With the Stripe CLI: `stripe trigger checkout.session.completed`
4. If your endpoint is set up correctly, you should see the Stripe events logged in your console with a status of `200`.
5. Then, check the [Trigger.dev](https://cloud.trigger.dev) dashboard and you should see the successful run of the `stripe-webhook` task.
For more information on setting up and testing Stripe webhooks, refer to the [Stripe Webhook Documentation](https://stripe.com/docs/webhooks).
@@ -0,0 +1,208 @@
---
title: "Supabase database operations using Trigger.dev"
sidebarTitle: "Supabase database operations"
description: "These examples demonstrate how to run basic CRUD operations on a table in a Supabase database using Trigger.dev."
---
import SupabaseDocsCards from "/snippets/supabase-docs-cards.mdx";
import SupabaseAuthInfo from "/snippets/supabase-auth-info.mdx";
## Add a new user to a table in a Supabase database
This is a basic task which inserts a new row into a table from a Trigger.dev task.
### Key features
- Shows how to set up a Supabase client using the `@supabase/supabase-js` library
- Shows how to add a new row to a table using `insert`
### Prerequisites
- A [Supabase account](https://supabase.com/dashboard/) and a project set up
- In your Supabase project, create a table called `user_subscriptions`.
- In your `user_subscriptions` table, create a new column:
- `user_id`, with the data type: `text`
### Task code
```ts trigger/supabase-database-insert.ts
import { createClient } from "@supabase/supabase-js";
import { task } from "@trigger.dev/sdk";
import jwt from "jsonwebtoken";
// Generate the Typescript types using the Supabase CLI: https://supabase.com/docs/guides/api/rest/generating-types
import { Database } from "database.types";
export const supabaseDatabaseInsert = task({
id: "add-new-user",
run: async (payload: { userId: string }) => {
const { userId } = payload;
// Get JWT secret from env vars
const jwtSecret = process.env.SUPABASE_JWT_SECRET;
if (!jwtSecret) {
throw new Error("SUPABASE_JWT_SECRET is not defined in environment variables");
}
// Create JWT token for the user
const token = jwt.sign({ sub: userId }, jwtSecret, { expiresIn: "1h" });
// Initialize Supabase client with JWT
const supabase = createClient<Database>(
process.env.SUPABASE_URL as string,
process.env.SUPABASE_ANON_KEY as string,
{
global: {
headers: {
Authorization: `Bearer ${token}`,
},
},
}
);
// Insert a new row into the user_subscriptions table with the provided userId
const { error } = await supabase.from("user_subscriptions").insert({
user_id: userId,
});
// If there was an error inserting the new user, throw an error
if (error) {
throw new Error(`Failed to insert new user: ${error.message}`);
}
return {
message: `New user added successfully: ${userId}`,
};
},
});
```
<SupabaseAuthInfo />
### Testing your task
To test this task in the [Trigger.dev dashboard](https://cloud.trigger.dev), you can use the following payload:
```json
{
"userId": "user_12345"
}
```
If the task completes successfully, you will see a new row in your `user_subscriptions` table with the `user_id` set to `user_12345`.
## Update a user's subscription on a table in a Supabase database
This task shows how to update a user's subscription on a table. It checks if the user already has a subscription and either inserts a new row or updates an existing row with the new plan.
This type of task is useful for managing user subscriptions, updating user details, or performing other operations you might need to do on a database table.
### Key features
- Shows how to set up a Supabase client using the `@supabase/supabase-js` library
- Adds a new row to the table if the user doesn't exist using `insert`
- Checks if the user already has a plan, and if they do updates the existing row using `update`
- Demonstrates how to use [AbortTaskRunError](https://trigger.dev/docs/errors-retrying#using-aborttaskrunerror) to stop the task run without retrying if an invalid plan type is provided
### Prerequisites
- A [Supabase account](https://supabase.com/dashboard/) and a project set up
- In your Supabase project, create a table called `user_subscriptions` (if you haven't already)
- In your `user_subscriptions` table, create these columns (if they don't already exist):
- `user_id`, with the data type: `text`
- `plan`, with the data type: `text`
- `updated_at`, with the data type: `timestamptz`
### Task code
```ts trigger/supabase-update-user-subscription.ts
import { createClient } from "@supabase/supabase-js";
import { AbortTaskRunError, task } from "@trigger.dev/sdk";
// Generate the Typescript types using the Supabase CLI: https://supabase.com/docs/guides/api/rest/generating-types
import { Database } from "database.types";
// Define the allowed plan types
type PlanType = "hobby" | "pro" | "enterprise";
// Create a single Supabase client for interacting with your database
// 'Database' supplies the type definitions to supabase-js
const supabase = createClient<Database>(
// These details can be found in your Supabase project settings under `API`
process.env.SUPABASE_PROJECT_URL as string, // e.g. https://abc123.supabase.co - replace 'abc123' with your project ID
process.env.SUPABASE_SERVICE_ROLE_KEY as string // Your service role secret key
);
export const supabaseUpdateUserSubscription = task({
id: "update-user-subscription",
run: async (payload: { userId: string; newPlan: PlanType }) => {
const { userId, newPlan } = payload;
// Abort the task run without retrying if the new plan type is invalid
if (!["hobby", "pro", "enterprise"].includes(newPlan)) {
throw new AbortTaskRunError(
`Invalid plan type: ${newPlan}. Allowed types are 'hobby', 'pro', or 'enterprise'.`
);
}
// Query the user_subscriptions table to check if the user already has a subscription
const { data: existingSubscriptions } = await supabase
.from("user_subscriptions")
.select("user_id")
.eq("user_id", userId);
if (!existingSubscriptions || existingSubscriptions.length === 0) {
// If there are no existing users with the provided userId and plan, insert a new row
const { error: insertError } = await supabase.from("user_subscriptions").insert({
user_id: userId,
plan: newPlan,
updated_at: new Date().toISOString(),
});
// If there was an error inserting the new subscription, throw an error
if (insertError) {
throw new Error(`Failed to insert user subscription: ${insertError.message}`);
}
} else {
// If the user already has a subscription, update their existing row
const { error: updateError } = await supabase
.from("user_subscriptions")
// Set the plan to the new plan and update the timestamp
.update({ plan: newPlan, updated_at: new Date().toISOString() })
.eq("user_id", userId);
// If there was an error updating the subscription, throw an error
if (updateError) {
throw new Error(`Failed to update user subscription: ${updateError.message}`);
}
}
// Return an object with the userId and newPlan
return {
userId,
newPlan,
};
},
});
```
<Note>
This task uses your service role secret key to bypass Row Level Security. There are different ways
of configuring your [RLS
policies](https://supabase.com/docs/guides/database/postgres/row-level-security), so always make
sure you have the correct permissions set up for your project.
</Note>
### Testing your task
To test this task in the [Trigger.dev dashboard](https://cloud.trigger.dev), you can use the following payload:
```json
{
"userId": "user_12345",
"newPlan": "pro"
}
```
If the task completes successfully, you will see a new row in your `user_subscriptions` table with the `user_id` set to `user_12345`, the `plan` set to `pro`, and the `updated_at` timestamp updated to the current time.
<SupabaseDocsCards />
@@ -0,0 +1,153 @@
---
title: "Uploading files to Supabase Storage"
sidebarTitle: "Supabase Storage upload"
description: "This example demonstrates how to upload files to Supabase Storage using Trigger.dev."
---
import SupabaseDocsCards from "/snippets/supabase-docs-cards.mdx";
import SupabaseAuthInfo from "/snippets/supabase-auth-info.mdx";
## Overview
This example shows how to upload a video file to Supabase Storage using two different methods.
- [Upload to Supabase Storage using the Supabase client](/guides/examples/supabase-storage-upload#example-1-upload-to-supabase-storage-using-the-supabase-storage-client)
- [Upload to Supabase Storage using the AWS S3 client](/guides/examples/supabase-storage-upload#example-2-upload-to-supabase-storage-using-the-aws-s3-client)
## Upload to Supabase Storage using the Supabase client
This task downloads a video from a provided URL and uploads it to Supabase Storage using the Supabase client.
### Task code
```ts trigger/supabase-storage-upload.ts
import { createClient } from "@supabase/supabase-js";
import { logger, task } from "@trigger.dev/sdk";
import fetch from "node-fetch";
// Initialize Supabase client
const supabase = createClient(
process.env.SUPABASE_PROJECT_URL ?? "",
process.env.SUPABASE_SERVICE_ROLE_KEY ?? ""
);
export const supabaseStorageUpload = task({
id: "supabase-storage-upload",
run: async (payload: { videoUrl: string }) => {
const { videoUrl } = payload;
const bucket = "my_bucket"; // Replace "my_bucket" with your bucket name
const objectKey = `video_${Date.now()}.mp4`;
// Download video data as a buffer
const response = await fetch(videoUrl);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const videoBuffer = await response.buffer();
// Upload the video directly to Supabase Storage
const { error } = await supabase.storage.from(bucket).upload(objectKey, videoBuffer, {
contentType: "video/mp4",
upsert: true,
});
if (error) {
throw new Error(`Error uploading video: ${error.message}`);
}
logger.log(`Video uploaded to Supabase Storage bucket`, { objectKey });
// Return the video object key and bucket
return {
objectKey,
bucket: bucket,
};
},
});
```
### Testing your task
To test this task in the dashboard, you can use the following payload:
```json
{
"videoUrl": "<a-video-url>" // Replace <a-video-url> with the URL of the video you want to upload
}
```
## Upload to Supabase Storage using the AWS S3 client
This task downloads a video from a provided URL, saves it to a temporary file, and then uploads the video file to Supabase Storage using the AWS S3 client.
### Key features
- Fetches a video from a provided URL
- Uploads the video file to Supabase Storage using S3
### Task code
```ts trigger/supabase-storage-upload-s3.ts
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { logger, task } from "@trigger.dev/sdk";
import fetch from "node-fetch";
// Initialize S3 client for Supabase Storage
const s3Client = new S3Client({
region: process.env.SUPABASE_REGION, // Your Supabase project's region e.g. "us-east-1"
endpoint: `https://${process.env.SUPABASE_PROJECT_ID}.supabase.co/storage/v1/s3`,
credentials: {
// These credentials can be found in your supabase storage settings, under 'S3 access keys'
accessKeyId: process.env.SUPABASE_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.SUPABASE_SECRET_ACCESS_KEY ?? "",
},
});
export const supabaseStorageUploadS3 = task({
id: "supabase-storage-upload-s3",
run: async (payload: { videoUrl: string }) => {
const { videoUrl } = payload;
// Fetch the video as an ArrayBuffer
const response = await fetch(videoUrl);
const videoArrayBuffer = await response.arrayBuffer();
const videoBuffer = Buffer.from(videoArrayBuffer);
const bucket = "my_bucket"; // Replace "my_bucket" with your bucket name
const objectKey = `video_${Date.now()}.mp4`;
// Upload the video directly to Supabase Storage
await s3Client.send(
new PutObjectCommand({
Bucket: bucket,
Key: objectKey,
Body: videoBuffer,
})
);
logger.log(`Video uploaded to Supabase Storage bucket`, { objectKey });
// Return the video object key
return {
objectKey,
bucket: bucket,
};
},
});
```
<SupabaseAuthInfo />
### Testing your task
To test this task in the dashboard, you can use the following payload:
```json
{
"videoUrl": "<a-video-url>" // Replace <a-video-url> with the URL of the video you want to upload
}
```
<SupabaseDocsCards />
+57
View File
@@ -0,0 +1,57 @@
---
title: "Using the Vercel AI SDK"
sidebarTitle: "Vercel AI SDK"
description: "This example demonstrates how to use the Vercel AI SDK with Trigger.dev."
---
import VercelDocsCards from "/snippets/vercel-docs-cards.mdx";
## Overview
The [Vercel AI SDK](https://www.npmjs.com/package/ai) is a simple way to use AI models from many different providers, including OpenAI, Microsoft Azure, Google Generative AI, Anthropic, Amazon Bedrock, Groq, Perplexity and [more](https://sdk.vercel.ai/providers/ai-sdk-providers).
It provides a consistent interface to interact with the different AI models, so you can easily switch between them without needing to change your code.
## Generate text using OpenAI
This task shows how to use the Vercel AI SDK to generate text from a prompt with OpenAI.
### Task code
```ts trigger/vercel-ai-sdk-openai.ts
import { logger, task } from "@trigger.dev/sdk";
import { generateText } from "ai";
// Install the package of the AI model you want to use, in this case OpenAI
import { openai } from "@ai-sdk/openai"; // Ensure OPENAI_API_KEY environment variable is set
export const openaiTask = task({
id: "openai-text-generate",
run: async (payload: { prompt: string }) => {
const chatCompletion = await generateText({
model: openai("gpt-4-turbo"),
// Add a system message which will be included with the prompt
system: "You are a friendly assistant!",
// The prompt passed in from the payload
prompt: payload.prompt,
});
// Log the generated text
logger.log("chatCompletion text:" + chatCompletion.text);
return chatCompletion;
},
});
```
## Testing your task
To test this task in the dashboard, you can use the following payload:
```json
{
"prompt": "What is the meaning of life?"
}
```
<VercelDocsCards />
@@ -0,0 +1,81 @@
---
title: "Syncing environment variables from your Vercel projects"
sidebarTitle: "Vercel sync env vars"
description: "This example demonstrates how to sync environment variables from your Vercel project to Trigger.dev."
---
import VercelDocsCards from "/snippets/vercel-docs-cards.mdx";
<Warning>
**Deprecated when using the Vercel integration.** If you are using the [Vercel
integration](/vercel-integration), do not use `syncVercelEnvVars` — the integration handles env
var syncing natively and using both together can cause env vars to be incorrectly populated.
If you are **not** using the Vercel integration, `syncVercelEnvVars` is still supported. Continue
with the configuration below.
</Warning>
## Build configuration
If you are not using the [Vercel integration](/vercel-integration), you can sync environment variables manually by adding the `syncVercelEnvVars` build extension to your `trigger.config.ts` file. This extension will run automatically every time you deploy your Trigger.dev project.
<Note>
You need to set the `VERCEL_ACCESS_TOKEN` and `VERCEL_PROJECT_ID` environment variables, or pass
in the token and project ID as arguments to the `syncVercelEnvVars` build extension. If you're
working with a team project, you'll also need to set `VERCEL_TEAM_ID`, which can be found in your
team settings. You can find / generate the `VERCEL_ACCESS_TOKEN` in your Vercel
[dashboard](https://vercel.com/account/settings/tokens). Make sure the scope of the token covers
the project with the environment variables you want to sync.
</Note>
<Note>
When running the build from a Vercel build environment (e.g., during a Vercel deployment), the
environment variable values will be read from `process.env` instead of fetching them from the
Vercel API. This is determined by checking if the `VERCEL` environment variable is present. The
API is still used to determine which environment variables are configured for your project, but
the actual values come from the local environment.
</Note>
```ts trigger.config.ts
import { defineConfig } from "@trigger.dev/sdk";
import { syncVercelEnvVars } from "@trigger.dev/build/extensions/core";
export default defineConfig({
project: "<project ref>",
// Your other config settings...
build: {
// Add the syncVercelEnvVars build extension
extensions: [
syncVercelEnvVars({
// A personal access token created in your Vercel account settings
// Used to authenticate API requests to Vercel
// Generate at: https://vercel.com/account/tokens
vercelAccessToken: process.env.VERCEL_ACCESS_TOKEN,
// The unique identifier of your Vercel project
// Found in Project Settings > General > Project ID
projectId: process.env.VERCEL_PROJECT_ID,
// Optional: The ID of your Vercel team
// Only required for team projects
// Found in Team Settings > General > Team ID
vercelTeamId: process.env.VERCEL_TEAM_ID,
}),
],
},
});
```
<Note>
[Build extensions](/config/extensions/overview) allow you to hook into the build system and
customize the build process or the resulting bundle and container image (in the case of
deploying). You can use pre-built extensions or create your own.
</Note>
## Running the sync operation
To sync the environment variables, all you need to do is run our `deploy` command. You should see some output in the console indicating that the environment variables have been synced, and they should now be available in your Trigger.dev dashboard.
```bash
npx trigger.dev@latest deploy
```
<VercelDocsCards />
+121
View File
@@ -0,0 +1,121 @@
---
title: "Bun guide"
sidebarTitle: "Bun"
description: "This guide will show you how to setup Trigger.dev in your existing Bun project, test an example task, and view the run."
icon: "js"
---
import Prerequisites from "/snippets/framework-prerequisites.mdx";
import CliRunTestStep from "/snippets/step-run-test.mdx";
import CliViewRunStep from "/snippets/step-view-run.mdx";
<Warning>
The trigger.dev CLI does not yet support Bun. So you will need to run the CLI using Node.js. Bun
will still be used to execute your tasks, even in the `dev` environment.
</Warning>
<Note>
**Supported Bun version:** Deployed tasks run on Bun 1.3.3. For local development, use Bun 1.3.x
for compatibility.
</Note>
<Prerequisites framework="Bun" />
## Known issues
- Certain OpenTelemetry instrumentation will not work with Bun, because Bun does not support Node's `register` hook. This means that some libraries that rely on this hook will not work with Bun.
- If Bun is installed via Homebrew (e.g. `/opt/homebrew/bin/bun`), you may see an `ENOENT: spawn /Users/<you>/.bun/bin/bun` error because the CLI expects Bun at the default install path. **Workaround:** create a symlink:
```bash
mkdir -p ~/.bun/bin && ln -s $(which bun) ~/.bun/bin/bun
```
- Bun's WebSocket client does not handle the `101 Switching Protocols` upgrade response correctly, so connecting to a remote browser via `puppeteer.connect()` / `playwright.connectOverCDP()` (e.g. BrowserBase, Browserless) fails silently — typically with an empty `{}` `ErrorEvent`. The remote session opens and immediately drops. **Workaround:** set `runtime: "node"` in `trigger.config.ts` for tasks that connect to a remote browser.
## Initial setup
<Steps>
<Step title="Run the CLI `init` command">
The easiest way to get started is to use the CLI. It will add Trigger.dev to your existing project, create a `/trigger` folder and give you an example task.
Run this command in the root of your project to get started:
<CodeGroup>
```bash npm
npx trigger.dev@latest init --runtime bun
```
```bash pnpm
pnpm dlx trigger.dev@latest init --runtime bun
```
```bash yarn
yarn dlx trigger.dev@latest init --runtime bun
```
</CodeGroup>
It will do a few things:
1. Log you into the CLI if you're not already logged in.
2. Create a `trigger.config.ts` file in the root of your project.
3. Ask where you'd like to create the `/trigger` directory.
4. Create the `/src/trigger` directory with an example task, `/src/trigger/example.[ts/js]`.
Install the "Hello World" example task when prompted. We'll use this task to test the setup.
</Step>
<Step title="Update example.ts to use Bun">
Open the `/src/trigger/example.ts` file and replace the contents with the following:
```ts example.ts
import { Database } from "bun:sqlite";
import { task } from "@trigger.dev/sdk";
export const bunTask = task({
id: "bun-task",
run: async (payload: { query: string }) => {
const db = new Database(":memory:");
const query = db.query("select 'Hello world' as message;");
console.log(query.get()); // => { message: "Hello world" }
return {
message: "Query executed",
};
},
});
```
</Step>
<Step title="Run the CLI `dev` command">
The CLI `dev` command runs a server for your tasks. It watches for changes in your `/trigger` directory and communicates with the Trigger.dev platform to register your tasks, perform runs, and send data back and forth.
It can also update your `@trigger.dev/*` packages to prevent version mismatches and failed deploys. You will always be prompted first.
<CodeGroup>
```bash npm
npx trigger.dev@latest dev
```
```bash pnpm
pnpm dlx trigger.dev@latest dev
```
```bash yarn
yarn dlx trigger.dev@latest dev
```
</CodeGroup>
</Step>
<CliRunTestStep />
<CliViewRunStep />
</Steps>
+154
View File
@@ -0,0 +1,154 @@
---
title: "Drizzle setup guide"
sidebarTitle: "Drizzle setup guide"
description: "This guide will show you how to set up Drizzle ORM with Trigger.dev"
icon: "D"
---
import Prerequisites from "/snippets/framework-prerequisites.mdx";
import CliInitStep from "/snippets/step-cli-init.mdx";
import CliDevStep from "/snippets/step-cli-dev.mdx";
import CliRunTestStep from "/snippets/step-run-test.mdx";
import CliViewRunStep from "/snippets/step-view-run.mdx";
import UsefulNextSteps from "/snippets/useful-next-steps.mdx";
## Overview
This guide will show you how to set up [Drizzle ORM](https://orm.drizzle.team/) with Trigger.dev, test and view an example task run.
## Prerequisites
- An existing Node.js project with a `package.json` file
- Ensure TypeScript is installed
- A [PostgreSQL](https://www.postgresql.org/) database server running locally, or accessible via a connection string
- Drizzle ORM [installed and initialized](https://orm.drizzle.team/docs/get-started) in your project
- A `DATABASE_URL` environment variable set in your `.env` file, pointing to your PostgreSQL database (e.g. `postgresql://user:password@localhost:5432/dbname`)
<Tip>
If your Postgres lives in a private AWS VPC (e.g. RDS without a public endpoint), connect it via
[Private networking](/private-networking/overview) instead of opening it to the public internet
(Pro and Enterprise plans).
</Tip>
## Initial setup (optional)
Follow these steps if you don't already have Trigger.dev set up in your project.
<Steps>
<CliInitStep />
<CliDevStep />
<CliRunTestStep />
<CliViewRunStep />
</Steps>
## Creating a task using Drizzle and deploying it to production
<Steps>
<Step title="The task using Drizzle">
First, create a new task file in your `trigger` folder.
This is a simple task that will add a new user to your database, we will call it `drizzle-add-new-user`.
<Note>
For this task to work correctly, you will need to have a `users` table schema defined with Drizzle
that includes `name`, `age` and `email` fields.
</Note>
```ts /trigger/drizzle-add-new-user.ts
import { eq } from "drizzle-orm";
import { task } from "@trigger.dev/sdk";
import { users } from "src/db/schema";
import { drizzle } from "drizzle-orm/node-postgres";
// Initialize Drizzle client
const db = drizzle(process.env.DATABASE_URL!);
export const addNewUser = task({
id: "drizzle-add-new-user",
run: async (payload: typeof users.$inferInsert) => {
// Create new user
const [user] = await db.insert(users).values(payload).returning();
return {
createdUser: user,
message: "User created and updated successfully",
};
},
});
```
</Step>
<Step title="Configuring the build">
Next, in your `trigger.config.js` file, add `pg` to the `externals` array. `pg` is a non-blocking PostgreSQL client for Node.js.
It is marked as an external to ensure that it is not bundled into the task's bundle, and instead will be installed and loaded from `node_modules` at runtime.
```js /trigger.config.js
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
project: "<project ref>", // Your project reference
// Your other config settings...
build: {
externals: ["pg"],
},
});
```
</Step>
<Step title="Deploying your task">
Once the build configuration is added, you can now deploy your task using the Trigger.dev CLI.
<CodeGroup>
```bash npm
npx trigger.dev@latest deploy
```
```bash pnpm
pnpm dlx trigger.dev@latest deploy
```
```bash yarn
yarn dlx trigger.dev@latest deploy
```
</CodeGroup>
</Step>
<Step title="Adding your DATABASE_URL environment variable to Trigger.dev">
In your Trigger.dev dashboard sidebar click "Environment Variables" <Icon icon="circle-1" iconType="solid" size={20} color="A8FF53" />, and then the "New environment variable" button <Icon icon="circle-2" iconType="solid" size={20} color="A8FF53" />.
![Environment variables page](/images/environment-variables-page.jpg)
You can add values for your local dev environment, staging and prod. in this case we will add the `DATABASE_URL` for the production environment.
![Environment variables
page](/images/environment-variables-panel.jpg)
</Step>
<Step title="Running your task">
To test this task, go to the 'test' page in the Trigger.dev dashboard and run the task with the following payload:
```json
{
"name": "<a-name>", // e.g. "John Doe"
"age": "<an-age>", // e.g. 25
"email": "<an-email>" // e.g. "john@doe.test"
}
```
Congratulations! You should now see a new completed run, and a new user with the credentials you provided should be added to your database.
</Step>
</Steps>
<UsefulNextSteps />
+287
View File
@@ -0,0 +1,287 @@
---
title: "Nango OAuth with Trigger.dev"
sidebarTitle: "Nango OAuth guide"
description: "Use Nango to authenticate API calls inside a Trigger.dev task, no token management required."
icon: "key"
---
[Nango](https://www.nango.dev/) handles OAuth for 250+ APIs, storing and automatically refreshing access tokens on your behalf. This makes it a natural fit for Trigger.dev tasks that need to call third-party APIs on behalf of your users.
In this guide you'll build a task that:
1. Receives a Nango `connectionId` from your frontend
2. Fetches a fresh GitHub access token from Nango inside the task
3. Calls the GitHub API to retrieve the user's open pull requests
4. Uses Claude to summarize what's being worked on
This pattern works for any API Nango supports. Swap GitHub for HubSpot, Slack, Notion, or any other provider.
## Prerequisites
- A Next.js project with [Trigger.dev installed](/guides/frameworks/nextjs)
- A [Nango](https://app.nango.dev/) account
- An [Anthropic](https://console.anthropic.com/) API key
## How it works
```mermaid
sequenceDiagram
participant User
participant Frontend
participant API as Next.js API
participant TD as Trigger.dev task
participant Nango
participant GH as GitHub API
participant Claude
User->>Frontend: Clicks "Analyze my PRs"
Frontend->>API: POST /api/nango-session
API->>Nango: POST /connect/sessions (secret key)
Nango-->>API: session token (30 min TTL)
API-->>Frontend: session token
Frontend->>Nango: OAuth connect (frontend SDK + session token)
Nango-->>Frontend: connectionId
Frontend->>API: POST /api/analyze-prs { connectionId, repo }
API->>TD: tasks.trigger(...)
TD->>Nango: getConnection(connectionId)
Nango-->>TD: access_token
TD->>GH: GET /repos/:repo/pulls
GH-->>TD: open pull requests
TD->>Claude: Summarize PRs
Claude-->>TD: Summary
```
## Step 1: Connect GitHub in Nango
<Steps titleSize="h3">
<Step title="Create a GitHub integration in Nango">
1. In your [Nango dashboard](https://app.nango.dev/), go to **Integrations** and click **Set up new integration**.
2. Search for **GitHub** and select GitHub (User OAuth).
3. Create and add a test connection
</Step>
<Step title="Add the Nango frontend SDK">
Install the Nango frontend SDK in your Next.js project:
```bash
npm install @nangohq/frontend
```
The frontend SDK requires a short-lived **connect session token** issued by your backend. Add an API route that creates the session:
```ts app/api/nango-session/route.ts
import { NextResponse } from "next/server";
export async function POST(req: Request) {
const { userId } = await req.json();
if (!userId || typeof userId !== "string") {
return NextResponse.json({ error: "Missing or invalid userId" }, { status: 400 });
}
const response = await fetch("https://api.nango.dev/connect/sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NANGO_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
end_user: { id: userId },
}),
});
if (!response.ok) {
const text = await response.text();
console.error("Nango error:", response.status, text);
return NextResponse.json({ error: text }, { status: response.status });
}
const { data } = await response.json();
return NextResponse.json({ token: data.token });
}
```
Then add a connect button to your UI that fetches the token and opens the Nango OAuth flow:
```tsx app/page.tsx
"use client";
import Nango from "@nangohq/frontend";
export default function Page() {
async function connectGitHub() {
// Get a short-lived session token from your backend
const sessionRes = await fetch("/api/nango-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId: "user_123" }), // replace with your actual user ID
});
const { token } = await sessionRes.json();
const nango = new Nango({ connectSessionToken: token });
// Use the exact integration slug from your Nango dashboard
const result = await nango.auth("<your-integration-slug>");
// result.connectionId is what you pass to your task
await fetch("/api/analyze-prs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
connectionId: result.connectionId,
repo: "triggerdotdev/trigger.dev",
}),
});
}
return <button onClick={connectGitHub}>Analyze my PRs</button>;
}
```
</Step>
</Steps>
## Step 2: Create the Trigger.dev task
Install the required packages:
```bash
npm install @nangohq/node @anthropic-ai/sdk
```
Create the task:
<CodeGroup>
```ts trigger/analyze-prs.ts
import { task } from "@trigger.dev/sdk";
import { Nango } from "@nangohq/node";
import Anthropic from "@anthropic-ai/sdk";
const nango = new Nango({ secretKey: process.env.NANGO_SECRET_KEY! });
const anthropic = new Anthropic();
export const analyzePRs = task({
id: "analyze-prs",
run: async (payload: { connectionId: string; repo: string }) => {
const { connectionId, repo } = payload;
// Fetch a fresh access token from Nango. It handles refresh automatically.
// Use the exact integration slug from your Nango dashboard, e.g. "github-getting-started"
const connection = await nango.getConnection("<your-integration-slug>", connectionId);
if (connection.credentials.type !== "OAUTH2") {
throw new Error(`Unexpected credentials type: ${connection.credentials.type}`);
}
const accessToken = connection.credentials.access_token;
// Call the GitHub API on behalf of the user
const response = await fetch(
`https://api.github.com/repos/${repo}/pulls?state=open&per_page=20`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/vnd.github.v3+json",
},
}
);
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
}
const prs = await response.json();
if (prs.length === 0) {
return { summary: "No open pull requests found.", prCount: 0 };
}
// Use Claude to summarize what's being worked on
const prList = prs
.map(
(pr: { number: number; title: string; user: { login: string }; body: string | null }) =>
`#${pr.number} by @${pr.user.login}: ${pr.title}\n${pr.body?.slice(0, 200) ?? ""}`
)
.join("\n\n");
const message = await anthropic.messages.create({
model: "claude-opus-4-6",
max_tokens: 1024,
messages: [
{
role: "user",
content: `Here are the open pull requests for ${repo}. Give a concise summary of what's being worked on, grouped by theme where possible.\n\n${prList}`,
},
],
});
const summary = message.content[0].type === "text" ? message.content[0].text : "";
return { summary, prCount: prs.length };
},
});
```
</CodeGroup>
## Step 3: Create the API route
Add a route handler that receives the `connectionId` from your frontend and triggers the task:
```ts app/api/analyze-prs/route.ts
import { analyzePRs } from "@/trigger/analyze-prs";
import { NextResponse } from "next/server";
export async function POST(req: Request) {
const { connectionId, repo } = await req.json();
if (!connectionId || !repo) {
return NextResponse.json({ error: "Missing connectionId or repo" }, { status: 400 });
}
const handle = await analyzePRs.trigger({ connectionId, repo });
return NextResponse.json(handle);
}
```
## Step 4: Set environment variables
Add the following to your `.env.local` file:
```bash
NANGO_SECRET_KEY= # From Nango dashboard → Environment → Secret key
TRIGGER_SECRET_KEY= # From Trigger.dev dashboard → API keys
ANTHROPIC_API_KEY= # From Anthropic console
```
Add `NANGO_SECRET_KEY` and `ANTHROPIC_API_KEY` as [environment variables](/deploy-environment-variables) in your Trigger.dev project too. These are used inside the task at runtime.
## Test it
<Steps titleSize="h3">
<Step title="Start your dev servers">
```bash
npm run dev
npx trigger.dev@latest dev
```
</Step>
<Step title="Connect GitHub and trigger the task">
Open your app, click **Analyze my PRs**, and complete the GitHub OAuth flow. The task will be triggered automatically.
</Step>
<Step title="Watch the run in Trigger.dev">
Open your [Trigger.dev dashboard](https://cloud.trigger.dev/) and navigate to **Runs** to see the task execute. You'll see the PR count and Claude's summary in the output.
</Step>
</Steps>
<Check>
Your task is now fetching a fresh GitHub token from Nango, calling the GitHub API on behalf of the
user, and using Claude to summarize their open PRs. No token storage or refresh logic required.
</Check>
## Next steps
- **Reuse the `connectionId`**: Once a user has connected, store their `connectionId` and pass it in future task payloads. No need to re-authenticate.
- **Add retries**: If the GitHub API returns a transient error, Trigger.dev [retries](/errors-retrying) will handle it automatically.
- **Switch providers**: The same pattern works for any Nango-supported API. Change `"github"` to `"hubspot"`, `"slack"`, `"notion"`, or any other provider.
- **Stream the analysis**: Use [Trigger.dev Realtime](/realtime/overview) to stream Claude's response back to your frontend as it's generated.
+152
View File
@@ -0,0 +1,152 @@
---
title: "Triggering tasks with webhooks in Next.js"
sidebarTitle: "Next.js webhooks"
description: "Learn how to trigger a task from a webhook in a Next.js app."
---
import VercelDocsCards from "/snippets/vercel-docs-cards.mdx";
## Prerequisites
- [A Next.js project, set up with Trigger.dev](/guides/frameworks/nextjs)
- [cURL](https://curl.se/) installed on your local machine. This will be used to send a POST request to your webhook handler.
## GitHub repo
<Card
title="View the project on GitHub"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/nextjs-webhooks/my-app"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## Adding the webhook handler
The webhook handler in this guide will be an API route.
This will be different depending on whether you are using the Next.js pages router or the app router.
### Pages router: creating the webhook handler
Create a new file `pages/api/webhook-handler.ts` or `pages/api/webhook-hander.js`.
In your new file, add the following code:
```ts /pages/api/webhook-handler.ts
import { helloWorldTask } from "@/trigger/example";
import { tasks } from "@trigger.dev/sdk";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// Parse the webhook payload
const payload = req.body;
// Trigger the helloWorldTask with the webhook data as the payload
await tasks.trigger<typeof helloWorldTask>("hello-world", payload);
res.status(200).json({ message: "OK" });
}
```
This code will handle the webhook payload and trigger the 'Hello World' task.
### App router: creating the webhook handler
Create a new file in the `app/api/webhook-handler/route.ts` or `app/api/webhook-handler/route.js`.
In your new file, add the following code:
```ts /app/api/webhook-handler/route.ts
import type { helloWorldTask } from "@/trigger/example";
import { tasks } from "@trigger.dev/sdk";
import { NextResponse } from "next/server";
export async function POST(req: Request) {
// Parse the webhook payload
const payload = await req.json();
// Trigger the helloWorldTask with the webhook data as the payload
await tasks.trigger<typeof helloWorldTask>("hello-world", payload);
return NextResponse.json("OK", { status: 200 });
}
```
This code will handle the webhook payload and trigger the 'Hello World' task.
## Triggering the task locally
Now that you have your webhook handler set up, you can trigger the 'Hello World' task from it. We will do this locally using cURL.
<Steps>
<Step title="Run your Next.js app and the Trigger.dev dev server">
First, run your Next.js app.
<CodeGroup>
```bash npm
npm run dev
```
```bash pnpm
pnpm run dev
```
```bash yarn
yarn dev
```
</CodeGroup>
Then, open up a second terminal window and start the Trigger.dev dev server:
<CodeGroup>
```bash npm
npx trigger.dev@latest dev
```
```bash pnpm
pnpm dlx trigger.dev@latest dev
```
```bash yarn
yarn dlx trigger.dev@latest dev
```
</CodeGroup>
</Step>
<Step title="Trigger the webhook with some dummy data">
To send a POST request to your webhook handler, open up a terminal window on your local machine and run the following command:
<Tip>
If `http://localhost:3000` isn't the URL of your locally running Next.js app, replace the URL in
the below command with that URL instead.
</Tip>
```bash
curl -X POST -H "Content-Type: application/json" -d '{"Name": "John Doe", "Age": "87"}' http://localhost:3000/api/webhook-handler
```
This will send a POST request to your webhook handler, with a JSON payload.
</Step>
<Step title="Check the task ran successfully">
After running the command, you should see a successful dev run and a 200 response in your terminals.
If you now go to your [Trigger.dev dashboard](https://cloud.trigger.dev), you should also see a successful run for the 'Hello World' task, with the payload you sent, in this case; `{"name": "John Doe", "age": "87"}`.
</Step>
</Steps>
<VercelDocsCards />
+449
View File
@@ -0,0 +1,449 @@
---
title: "Next.js setup guide"
sidebarTitle: "Next.js"
description: "This guide will show you how to setup Trigger.dev in your existing Next.js project, test an example task, and view the run."
icon: "N"
---
import Prerequisites from "/snippets/framework-prerequisites.mdx";
import CliInitStep from "/snippets/step-cli-init.mdx";
import CliDevStep from "/snippets/step-cli-dev.mdx";
import CliRunTestStep from "/snippets/step-run-test.mdx";
import CliViewRunStep from "/snippets/step-view-run.mdx";
import UsefulNextSteps from "/snippets/useful-next-steps.mdx";
import TriggerTaskNextjs from "/snippets/trigger-tasks-nextjs.mdx";
import NextjsTroubleshootingMissingApiKey from "/snippets/nextjs-missing-api-key.mdx";
import NextjsTroubleshootingButtonSyntax from "/snippets/nextjs-button-syntax.mdx";
import AddEnvironmentVariables from "/snippets/add-environment-variables.mdx";
import DeployingYourTask from "/snippets/deplopying-your-task.mdx";
import VercelDocsCards from "/snippets/vercel-docs-cards.mdx";
import RunDevAndNextConcurrently from "/snippets/run-dev-and-next-concurrently.mdx";
<Note>This guide can be followed for both App and Pages router as well as Server Actions.</Note>
<Prerequisites framework="Next.js" />
## Initial setup
<Steps>
<CliInitStep />
<CliDevStep />
<CliRunTestStep />
<CliViewRunStep />
</Steps>
<RunDevAndNextConcurrently />
## Set your secret key locally
Set your `TRIGGER_SECRET_KEY` environment variable in your `.env.local` file if using the Next.js App router or `.env` file if using Pages router. This key is used to authenticate with Trigger.dev, so you can trigger runs from your Next.js app. Visit the API Keys page in the dashboard and select the DEV secret key.
![How to find your secret key](/images/api-keys.png)
For more information on authenticating with Trigger.dev, see the [API keys page](/apikeys).
## Triggering your task in Next.js
Here are the steps to trigger your task in the Next.js App and Pages router and Server Actions.
<Tabs>
<Tab title="App Router">
<Steps>
<Step title="Create a Route Handler">
Add a Route Handler by creating a `route.ts` file (or `route.js` file) in the `app/api` directory like this: `app/api/hello-world/route.ts`.
</Step>
<Step title="Add your task">
Add this code to your `route.ts` file which imports your task along with `NextResponse` to handle the API route response:
```ts app/api/hello-world/route.ts
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { helloWorldTask } from "@/trigger/example";
import { tasks } from "@trigger.dev/sdk";
import { NextResponse } from "next/server";
//tasks.trigger also works with the edge runtime
//export const runtime = "edge";
export async function GET() {
const handle = await tasks.trigger<typeof helloWorldTask>(
"hello-world",
"James"
);
return NextResponse.json(handle);
}
```
</Step>
<Step title="Trigger your task">
<TriggerTaskNextjs/>
</Step>
</Steps>
</Tab>
<Tab title="App Router (Server Actions)">
<Steps>
<Step title="Create an `actions.ts` file">
Create an `actions.ts` file in the `app/api` directory and add this code which imports your `helloWorldTask()` task. Make sure to include `"use server";` at the top of the file.
```ts app/api/actions.ts
"use server";
import type { helloWorldTask } from "@/trigger/example";
import { tasks } from "@trigger.dev/sdk";
export async function myTask() {
try {
const handle = await tasks.trigger<typeof helloWorldTask>(
"hello-world",
"James"
);
return { handle };
} catch (error) {
console.error(error);
return {
error: "something went wrong",
};
}
}
```
</Step>
<Step title="Create a button to trigger your task">
For the purposes of this guide, we'll create a button with an `onClick` event that triggers your task. We'll add this to the `page.tsx` file so we can trigger the task by clicking the button. Make sure to import your task and include `"use client";` at the top of your file.
```ts app/page.tsx
"use client";
import { myTask } from "./actions";
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-center p-24">
<button
onClick={async () => {
await myTask();
}}
>
Trigger my task
</button>
</main>
);
}
```
</Step>
<Step title="Trigger your task">
Run your Next.js app:
<CodeGroup>
```bash npm
npm run dev
```
```bash pnpm
pnpm run dev
```
```bash yarn
yarn dev
```
</CodeGroup>
Open your app in a browser, making sure the port number is the same as the one you're running your Next.js app on. For example, if you're running your Next.js app on port 3000, visit:
```bash
http://localhost:3000
```
Run the dev server from Step 2. of the [Initial Setup](/guides/frameworks/nextjs#initial-setup) section above if it's not already running:
<CodeGroup>
```bash npm
npx trigger.dev@latest dev
```
```bash pnpm
pnpm dlx trigger.dev@latest dev
```
```bash yarn
yarn dlx trigger.dev@latest dev
```
</CodeGroup>
Then click the button we created in your app to trigger the task. You should see the CLI log the task run with a link to view the logs.
![Trigger.dev CLI showing a successful run](/images/trigger-cli-run-success.png)
Visit the [Trigger.dev dashboard](https://cloud.trigger.dev) to see your run.
</Step>
</Steps>
</Tab>
<Tab title="Pages Router">
<Steps>
<Step title="Create an API route">
Create an API route in the `pages/api` directory. Then create a `hello-world .ts` (or `hello-world.js`) file for your task and copy this code example:
```ts pages/api/hello-world.ts
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { helloWorldTask } from "@/trigger/example";
import { tasks } from "@trigger.dev/sdk";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<{ id: string }>
) {
const handle = await tasks.trigger<typeof helloWorldTask>(
"hello-world",
"James"
);
res.status(200).json(handle);
}
```
</Step>
<Step title="Trigger your task">
<TriggerTaskNextjs/>
</Step>
</Steps>
</Tab>
</Tabs>
## Automatically sync environment variables from your Vercel project (optional)
If you want to automatically sync environment variables from your Vercel project to Trigger.dev, you can add our `syncVercelEnvVars` build extension to your `trigger.config.ts` file.
<Note>
You need to set the `VERCEL_ACCESS_TOKEN` and `VERCEL_PROJECT_ID` environment variables, or pass
in the token and project ID as arguments to the `syncVercelEnvVars` build extension. If you're
working with a team project, you'll also need to set `VERCEL_TEAM_ID`, which can be found in your
team settings. You can find / generate the `VERCEL_ACCESS_TOKEN` in your Vercel
[dashboard](https://vercel.com/account/settings/tokens). Make sure the scope of the token covers
the project with the environment variables you want to sync.
</Note>
```ts trigger.config.ts
import { defineConfig } from "@trigger.dev/sdk";
import { syncVercelEnvVars } from "@trigger.dev/build/extensions/core";
export default defineConfig({
project: "<project ref>",
// Your other config settings...
build: {
extensions: [syncVercelEnvVars()],
},
});
```
<Note>
For more information, see our [Vercel sync environment
variables](/guides/examples/vercel-sync-env-vars) guide.
</Note>
<AddEnvironmentVariables />
<DeployingYourTask />
## Troubleshooting & extra resources
### Revalidation from your Trigger.dev tasks
[Revalidation](https://vercel.com/docs/incremental-static-regeneration/quickstart#on-demand-revalidation) allows you to purge the cache for an ISR route. To revalidate an ISR route from a Trigger.dev task, you have to set up a handler for the `revalidate` event. This is an API route that you can add to your Next.js app.
This handler will run the `revalidatePath` function from Next.js, which purges the cache for the given path.
The handlers are slightly different for the App and Pages router:
#### Revalidation handler: App Router
If you are using the App router, create a new revalidation route at `app/api/revalidate/path/route.ts`:
```ts app/api/revalidate/path/route.ts
import { NextRequest, NextResponse } from "next/server";
import { revalidatePath } from "next/cache";
export async function POST(request: NextRequest) {
try {
const { path, type, secret } = await request.json();
// Create a REVALIDATION_SECRET and set it in your environment variables
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ message: "Invalid secret" }, { status: 401 });
}
if (!path) {
return NextResponse.json({ message: "Path is required" }, { status: 400 });
}
revalidatePath(path, type);
return NextResponse.json({ revalidated: true });
} catch (err) {
console.error("Error revalidating path:", err);
return NextResponse.json({ message: "Error revalidating path" }, { status: 500 });
}
}
```
#### Revalidation handler: Pages Router
If you are using the Pages router, create a new revalidation route at `pages/api/revalidate/path.ts`:
```ts pages/api/revalidate/path.ts
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
const { path, secret } = req.body;
if (secret !== process.env.REVALIDATION_SECRET) {
return res.status(401).json({ message: "Invalid secret" });
}
if (!path) {
return res.status(400).json({ message: "Path is required" });
}
await res.revalidate(path);
return res.json({ revalidated: true });
} catch (err) {
console.error("Error revalidating path:", err);
return res.status(500).json({ message: "Error revalidating path" });
}
}
```
#### Revalidation task
This task takes a `path` as a payload and will revalidate the path you specify, using the handler you set up previously.
<Note>
To run this task locally you will need to set the `REVALIDATION_SECRET` environment variable in your `.env.local` file (or `.env` file if using Pages router).
To run this task in production, you will need to set the `REVALIDATION_SECRET` environment variable in Vercel, in your project settings, and also in your environment variables in the Trigger.dev dashboard.
</Note>
```ts trigger/revalidate-path.ts
import { logger, task } from "@trigger.dev/sdk";
const NEXTJS_APP_URL = process.env.NEXTJS_APP_URL; // e.g. "http://localhost:3000" or "https://my-nextjs-app.vercel.app"
const REVALIDATION_SECRET = process.env.REVALIDATION_SECRET; // Create a REVALIDATION_SECRET and set it in your environment variables
export const revalidatePath = task({
id: "revalidate-path",
run: async (payload: { path: string }) => {
const { path } = payload;
try {
const response = await fetch(`${NEXTJS_APP_URL}/api/revalidate/path`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
path: `${NEXTJS_APP_URL}/${path}`,
secret: REVALIDATION_SECRET,
}),
});
if (response.ok) {
logger.log("Path revalidation successful", { path });
return { success: true };
} else {
logger.error("Path revalidation failed", {
path,
statusCode: response.status,
statusText: response.statusText,
});
return {
success: false,
error: `Revalidation failed with status ${response.status}: ${response.statusText}`,
};
}
} catch (error) {
logger.error("Path revalidation encountered an error", {
path,
error: error instanceof Error ? error.message : String(error),
});
return {
success: false,
error: `Failed to revalidate path due to an unexpected error`,
};
}
},
});
```
#### Testing the revalidation task
You can test your revalidation task in the Trigger.dev dashboard on the testing page, using the following payload.
```json
{
"path": "<path-to-revalidate>" // e.g. "blog"
}
```
<NextjsTroubleshootingMissingApiKey/>
<NextjsTroubleshootingButtonSyntax/>
## Realtime updates with React hooks
The `@trigger.dev/react-hooks` package lets you subscribe to task runs from your React components. Show progress bars, stream AI responses, or display run status in real time.
<CardGroup cols={2}>
<Card title="React hooks" icon="react" href="/realtime/react-hooks/overview">
Hooks for subscribing to runs, streaming data, and triggering tasks from the frontend.
</Card>
<Card title="Streams" icon="wave-pulse" href="/tasks/streams">
Pipe continuous data (like AI completions) from your tasks to the client while they run.
</Card>
</CardGroup>
<VercelDocsCards />
<UsefulNextSteps />
+26
View File
@@ -0,0 +1,26 @@
---
title: "Node.js setup guide"
sidebarTitle: "Node.js"
description: "This guide will show you how to setup Trigger.dev in your existing Node.js project, test an example task, and view the run."
icon: "node-js"
---
import Prerequisites from "/snippets/framework-prerequisites.mdx";
import CliInitStep from "/snippets/step-cli-init.mdx";
import CliDevStep from "/snippets/step-cli-dev.mdx";
import CliRunTestStep from "/snippets/step-run-test.mdx";
import CliViewRunStep from "/snippets/step-view-run.mdx";
import UsefulNextSteps from "/snippets/useful-next-steps.mdx";
<Prerequisites framework="Node.js" />
## Initial setup
<Steps>
<CliInitStep />
<CliDevStep />
<CliRunTestStep />
<CliViewRunStep />
</Steps>
<UsefulNextSteps />
+185
View File
@@ -0,0 +1,185 @@
---
title: "Prisma setup guide"
sidebarTitle: "Prisma setup guide"
description: "This guide will show you how to set up Prisma with Trigger.dev"
icon: "Triangle"
---
import Prerequisites from "/snippets/framework-prerequisites.mdx";
import CliInitStep from "/snippets/step-cli-init.mdx";
import CliDevStep from "/snippets/step-cli-dev.mdx";
import CliRunTestStep from "/snippets/step-run-test.mdx";
import CliViewRunStep from "/snippets/step-view-run.mdx";
import UsefulNextSteps from "/snippets/useful-next-steps.mdx";
## Overview
This guide will show you how to set up [Prisma](https://www.prisma.io/) with Trigger.dev, test and view an example task run.
## Prerequisites
- An existing Node.js project with a `package.json` file
- Ensure TypeScript is installed
- A [PostgreSQL](https://www.postgresql.org/) database server running locally, or accessible via a connection string
- Prisma ORM [installed and initialized](https://www.prisma.io/docs/getting-started/quickstart) in your project
- A `DATABASE_URL` environment variable set in your `.env` file, pointing to your PostgreSQL database (e.g. `postgresql://user:password@localhost:5432/dbname`)
## Initial setup (optional)
Follow these steps if you don't already have Trigger.dev set up in your project.
<Steps>
<CliInitStep />
<CliDevStep />
<CliRunTestStep />
<CliViewRunStep />
</Steps>
## Creating a task using Prisma and deploying it to production
<Steps>
<Step title="Writing the Prisma task">
First, create a new task file in your `trigger` folder.
This is a simple task that will add a new user to the database.
<Note>
For this task to work correctly, you will need to have a `user` model in your Prisma schema with
an `id` field, a `name` field, and an `email` field.
</Note>
```ts /trigger/prisma-add-new-user.ts
import { PrismaClient } from "@prisma/client";
import { task } from "@trigger.dev/sdk";
// Initialize Prisma client
const prisma = new PrismaClient();
export const addNewUser = task({
id: "prisma-add-new-user",
run: async (payload: { name: string; email: string; id: number }) => {
const { name, email, id } = payload;
// This will create a new user in the database
const user = await prisma.user.create({
data: {
name: name,
email: email,
id: id,
},
});
return {
message: `New user added successfully: ${user.id}`,
};
},
});
```
</Step>
<Step title="Configuring the build extension">
Next, configure the Prisma [build extension](https://trigger.dev/docs/config/extensions/overview) in the `trigger.config.js` file to include the Prisma client in the build.
This will ensure that the Prisma client is available when the task runs.
```js /trigger.config.js
export default defineConfig({
project: "<project ref>", // Your project reference
// Your other config settings...
build: {
extensions: [
prismaExtension({
mode: "legacy", // required
version: "5.20.0", // optional, we'll automatically detect the version if not provided
schema: "prisma/schema.prisma", // update this to the path of your Prisma schema file
}),
],
},
});
```
The `prismaExtension` requires a `mode` parameter. For standard Prisma setups, use `"legacy"`
mode. See the [Prisma extension documentation](/config/extensions/prismaExtension) for other modes
and full configuration options.
<Note>
[Build extensions](/config/extensions/overview) allow you to hook into the build system and
customize the build process or the resulting bundle and container image (in the case of
deploying). You can use pre-built extensions or create your own.
</Note>
</Step>
<Step title="Optional: adding Prisma instrumentation">
We use OpenTelemetry to [instrument](https://trigger.dev/docs/config/config-file#instrumentations) our tasks and collect telemetry data.
If you want to automatically log all Prisma queries and mutations, you can use the Prisma instrumentation extension.
```js /trigger.config.js
import { defineConfig } from "@trigger.dev/sdk";
import { PrismaInstrumentation } from "@prisma/instrumentation";
import { OpenAIInstrumentation } from "@traceloop/instrumentation-openai";
export default defineConfig({
//..other stuff
instrumentations: [new PrismaInstrumentation(), new OpenAIInstrumentation()],
});
```
This provides much more detailed information about your tasks with minimal effort.
</Step>
<Step title="Deploying your task">
With the build extension and task configured, you can now deploy your task using the Trigger.dev CLI.
<CodeGroup>
```bash npm
npx trigger.dev@latest deploy
```
```bash pnpm
pnpm dlx trigger.dev@latest deploy
```
```bash yarn
yarn dlx trigger.dev@latest deploy
```
</CodeGroup>
</Step>
<Step title="Adding your DATABASE_URL environment variable to Trigger.dev">
In your Trigger.dev dashboard sidebar click "Environment Variables" <Icon icon="circle-1" iconType="solid" size={20} color="A8FF53" />, and then the "New environment variable" button <Icon icon="circle-2" iconType="solid" size={20} color="A8FF53" />.
You can add values for your local dev environment, staging and prod. in this case we will add the `DATABASE_URL` for the production environment.
![Environment variables
page](/images/environment-variables-panel.jpg)
</Step>
<Step title="Running your task">
To test this task, go to the 'test' page in the Trigger.dev dashboard and run the task with the following payload:
```json
{
"name": "<a-name>", // e.g. "John Doe"
"email": "<a-email>", // e.g. "john@doe.test"
"id": <a-number> // e.g. 12345
}
```
Congratulations! You should now see a new completed run, and a new user with the credentials you provided should be added to your database.
</Step>
</Steps>
<UsefulNextSteps />
+117
View File
@@ -0,0 +1,117 @@
---
title: "Triggering tasks with webhooks in Remix"
sidebarTitle: "Remix webhooks"
description: "Learn how to trigger a task from a webhook in a Remix app."
---
## Prerequisites
- [A Remix project, set up with Trigger.dev](/guides/frameworks/remix)
- [cURL](https://curl.se/) installed on your local machine. This will be used to send a POST request to your webhook handler.
## GitHub repo
<Card
title="View the project on GitHub"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/remix-webhooks"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## Adding the webhook handler
The webhook handler in this guide will be an API route. Create a new file `app/routes/api.webhook-handler.ts` or `app/routes/api.webhook-handler.js`.
In your new file, add the following code:
```ts /api/webhook-handler.ts
import type { ActionFunctionArgs } from "@remix-run/node";
import { tasks } from "@trigger.dev/sdk";
import { helloWorldTask } from "src/trigger/example";
export async function action({ request }: ActionFunctionArgs) {
const payload = await request.json();
// Trigger the helloWorldTask with the webhook data as the payload
await tasks.trigger<typeof helloWorldTask>("hello-world", payload);
return new Response("OK", { status: 200 });
}
```
This code will handle the webhook payload and trigger the 'Hello World' task.
## Triggering the task locally
Now that you have a webhook handler set up, you can trigger the 'Hello World' task from it. We will do this locally using cURL.
<Steps>
<Step title="Run your Remix app and the Trigger.dev dev server">
First, run your Remix app.
<CodeGroup>
```bash npm
npm run dev
```
```bash pnpm
pnpm run dev
```
```bash yarn
yarn dev
```
</CodeGroup>
Then, open up a second terminal window and start the Trigger.dev dev server:
<CodeGroup>
```bash npm
npx trigger.dev@latest dev
```
```bash pnpm
pnpm dlx trigger.dev@latest dev
```
```bash yarn
yarn dlx trigger.dev@latest dev
```
</CodeGroup>
</Step>
<Step title="Trigger the webhook with some dummy data">
To send a POST request to your webhook handler, open up a terminal window on your local machine and run the following command:
<Tip>
If `http://localhost:5173` isn't the URL of your locally running Remix app, replace the URL in the
below command with that URL instead.
</Tip>
```bash
curl -X POST -H "Content-Type: application/json" -d '{"Name": "John Doe", "Age": "87"}' http://localhost:5173/api/webhook-handler
```
This will send a POST request to your webhook handler, with a JSON payload.
</Step>
<Step title="Check the task ran successfully">
After running the command, you should see a successful dev run and a 200 response in your terminals.
If you now go to your [Trigger.dev dashboard](https://cloud.trigger.dev), you should also see a successful run for the 'Hello World' task, with the payload you sent, in this case; `{"name": "John Doe", "age": "87"}`.
</Step>
</Steps>
+223
View File
@@ -0,0 +1,223 @@
---
title: "Remix setup guide"
sidebarTitle: "Remix"
description: "This guide will show you how to setup Trigger.dev in your existing Remix project, test an example task, and view the run."
icon: "r"
---
import Prerequisites from "/snippets/framework-prerequisites.mdx";
import CliInitStep from "/snippets/step-cli-init.mdx";
import CliDevStep from "/snippets/step-cli-dev.mdx";
import CliRunTestStep from "/snippets/step-run-test.mdx";
import CliViewRunStep from "/snippets/step-view-run.mdx";
import UsefulNextSteps from "/snippets/useful-next-steps.mdx";
import TriggerTaskRemix from "/snippets/trigger-tasks-remix.mdx";
import AddEnvironmentVariables from "/snippets/add-environment-variables.mdx";
import DeployingYourTask from "/snippets/deplopying-your-task.mdx";
<Prerequisites framework="Remix" />
## Initial setup
<Steps>
<CliInitStep />
<CliDevStep />
<CliRunTestStep />
<CliViewRunStep />
</Steps>
## Set your secret key locally
Set your `TRIGGER_SECRET_KEY` environment variable in your `.env` file. This key is used to authenticate with Trigger.dev, so you can trigger runs from your Remix app. Visit the API Keys page in the dashboard and select the DEV secret key.
![How to find your secret key](/images/api-keys.png)
For more information on authenticating with Trigger.dev, see the [API keys page](/apikeys).
## Triggering your task in Remix
<Steps>
<Step title="Create an API route">
Create a new file called `api.hello-world.ts` (or `api.hello-world.js`) in the `app/routes` directory like this: `app/routes/api.hello-world.ts`.
</Step>
<Step title="Add your task">
Add this code to your `api.hello-world.ts` file which imports your task:
```ts app/routes/api.hello-world.ts
import type { helloWorldTask } from "../../src/trigger/example";
import { tasks } from "@trigger.dev/sdk";
export async function loader() {
const handle = await tasks.trigger<typeof helloWorldTask>("hello-world", "James");
return new Response(JSON.stringify(handle), {
headers: { "Content-Type": "application/json" },
});
}
```
</Step>
<Step title="Trigger your task">
<TriggerTaskRemix/>
</Step>
</Steps>
<AddEnvironmentVariables />
<DeployingYourTask />
## Deploying to Vercel Edge Functions
Before we start, it's important to note that:
- We'll be using a type-only import for the task to ensure compatibility with the edge runtime.
- The `@trigger.dev/sdk` package supports the edge runtime out of the box.
There are a few extra steps to follow to deploy your `/api/hello-world` API endpoint to Vercel Edge Functions.
<Steps>
<Step title="Update your API route">
Update your API route to use the `runtime: "edge"` option and change it to an `action()` so we can trigger the task from a curl request later on.
```ts app/routes/api.hello-world.ts
import { tasks } from "@trigger.dev/sdk";
import type { helloWorldTask } from "../../src/trigger/example";
// 👆 **type-only** import
// include this at the top of your API route file
export const config = {
runtime: "edge",
};
export async function action({ request }: { request: Request }) {
// This is where you'd authenticate the request
const payload = await request.json();
const handle = await tasks.trigger<typeof helloWorldTask>("hello-world", payload);
return new Response(JSON.stringify(handle), {
headers: { "Content-Type": "application/json" },
});
}
```
</Step>
<Step title="Update the Vercel configuration">
Create or update the `vercel.json` file with the following:
```json vercel.json
{
"buildCommand": "npm run vercel-build",
"devCommand": "npm run dev",
"framework": "remix",
"installCommand": "npm install",
"outputDirectory": "build/client"
}
```
</Step>
<Step title="Update package.json scripts">
Update your `package.json` to include the following scripts:
```json package.json
"scripts": {
"build": "remix vite:build",
"dev": "remix vite:dev",
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
"start": "remix-serve ./build/server/index.js",
"typecheck": "tsc",
"vercel-build": "remix vite:build && cp -r ./public ./build/client"
},
```
</Step>
<Step title="Deploy to Vercel">
Push your code to a Git repository and create a new project in the Vercel dashboard. Select your repository and follow the prompts to complete the deployment.
</Step>
<Step title="Add your Vercel environment variables">
In the Vercel project settings, add your Trigger.dev secret key:
```bash
TRIGGER_SECRET_KEY=your-secret-key
```
You can find this key in the Trigger.dev dashboard under API Keys and select the environment key you want to use.
![How to find your secret key](/images/api-keys.png)
</Step>
<Step title="Deploy your project">
Once you've added the environment variable, deploy your project to Vercel.
<Note>
Ensure you have also deployed your Trigger.dev task. See [deploy your task
step](/guides/frameworks/remix#deploying-your-task-to-trigger-dev).
</Note>
</Step>
<Step title="Test your task in production">
After deployment, you can test your task in production by running this curl command:
```bash
curl -X POST https://your-app.vercel.app/api/hello-world \
-H "Content-Type: application/json" \
-d '{"name": "James"}'
```
This sends a POST request to your API endpoint with a JSON payload.
</Step>
</Steps>
### Additional notes
The `vercel-build` script in `package.json` is specific to Remix projects on Vercel, ensuring that static assets are correctly copied to the build output.
The `runtime: "edge"` configuration in the API route allows for better performance on Vercel's Edge Network.
## Realtime updates with React hooks
The `@trigger.dev/react-hooks` package lets you subscribe to task runs from your React components. Show progress bars, stream AI responses, or display run status in real time.
<CardGroup cols={2}>
<Card title="React hooks" icon="react" href="/realtime/react-hooks/overview">
Hooks for subscribing to runs, streaming data, and triggering tasks from the frontend.
</Card>
<Card title="Streams" icon="wave-pulse" href="/tasks/streams">
Pipe continuous data (like AI completions) from your tasks to the client while they run.
</Card>
</CardGroup>
## Additional resources for Remix
<Card
title="Remix - triggering tasks using webhooks"
icon="R"
href="/guides/frameworks/remix-webhooks"
>
How to create a webhook handler in a Remix app, and trigger a task from it.
</Card>
<UsefulNextSteps />
+328
View File
@@ -0,0 +1,328 @@
---
title: "Sequin database triggers"
sidebarTitle: "Sequin database triggers"
description: "This guide will show you how to trigger tasks from database changes using Sequin"
icon: "database"
---
[Sequin](https://sequinstream.com) allows you to trigger tasks from database changes. Sequin captures every insert, update, and delete on a table and then ensures a task is triggered for each change.
Often, task runs coincide with database changes. For instance, you might want to use a Trigger.dev task to generate an embedding for each post in your database:
<img src="/images/sequin-intro.png" alt="Sequin and Trigger.dev Overview" />
In this guide, you'll learn how to use Sequin to trigger Trigger.dev tasks from database changes.
## Prerequisites
You are about to create a [regular Trigger.dev task](/tasks-regular) that you will execute when ever a post is inserted or updated in your database. Sequin will detect all the changes on the `posts` table and then send the payload of the post to an API endpoint that will call `tasks.trigger()` to create the embedding and update the database.
As long as you create an HTTP endpoint that Sequin can deliver webhooks to, you can use any web framework or edge function (e.g. Supabase Edge Functions, Vercel Functions, Cloudflare Workers, etc.) to invoke your Trigger.dev task. In this guide, we'll show you how to setup Trigger.dev tasks using Next.js API Routes.
You'll need the following to follow this guide:
- A Next.js project with [Trigger.dev](https://trigger.dev) installed
<Info>
If you don't have one already, follow [Trigger.dev's Next.js setup
guide](/guides/frameworks/nextjs) to setup your project. You can return to this guide when
you're ready to write your first Trigger.dev task.
</Info>
- A [Sequin](https://console.sequinstream.com/register) account
- A Postgres database (Sequin works with any Postgres database version 12 and up) with a `posts` table.
## Create a Trigger.dev task
Start by creating a new Trigger.dev task that takes in a Sequin change event as a payload, creates an embedding, and then inserts the embedding into the database:
<Steps titleSize="h3">
<Step title="Create a `create-embedding-for-post` task">
In your `src/trigger/tasks` directory, create a new file called `create-embedding-for-post.ts` and add the following code:
<CodeGroup>
```ts trigger/create-embedding-for-post.ts
import { task } from "@trigger.dev/sdk";
import { OpenAI } from "openai";
import { upsertEmbedding } from "../util";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
export const createEmbeddingForPost = task({
id: "create-embedding-for-post",
run: async (payload: {
record: {
id: number;
title: string;
body: string;
author: string;
createdAt: string;
embedding: string | null;
},
metadata: {
table_schema: string,
table_name: string,
consumer: {
id: string;
name: string;
};
};
}) => {
// Create an embedding using the title and body of payload.record
const content = `${payload.record.title}\n\n${payload.record.body}`;
const embedding = (await openai.embeddings.create({
model: "text-embedding-ada-002",
input: content,
})).data[0].embedding;
// Upsert the embedding in the database. See utils.ts for the implementation -> ->
await upsertEmbedding(embedding, payload.record.id);
// Return the updated record
return {
...payload.record,
embedding: JSON.stringify(embedding),
};
}
});
````
```ts utils.ts
import pg from "pg";
export async function upsertEmbedding(embedding: number[], id: number) {
const client = new pg.Client({
connectionString: process.env.DATABASE_URL,
});
await client.connect();
try {
const query = `
INSERT INTO post_embeddings (id, embedding)
VALUES ($2, $1)
ON CONFLICT (id)
DO UPDATE SET embedding = $1
`;
const values = [JSON.stringify(embedding), id];
const result = await client.query(query, values);
console.log(`Updated record in database. Rows affected: ${result.rowCount}`);
return result.rowCount;
} catch (error) {
console.error("Error updating record in database:", error);
throw error;
} finally {
await client.end();
}
}
````
</CodeGroup>
This task takes in a Sequin record event, creates an embedding, and then upserts the embedding into a `post_embeddings` table.
</Step>
<Step title="Add the task to your Trigger.dev project">
Register the `create-embedding-for-post` task to your Trigger.dev cloud project by running the following command:
```bash
npx trigger.dev@latest dev
```
In the Trigger.dev dashboard, you should now see the `create-embedding-for-post` task:
<Frame>
<img src="/images/sequin-register-task.png" alt="Task added" />
</Frame>
</Step>
</Steps>
<Check>
You've successfully created a Trigger.dev task that will create an embedding for each post in your
database. In the next step, you'll create an API endpoint that Sequin can deliver records to.
</Check>
## Setup API route
You'll now create an API endpoint that will receive posts from Sequin and then trigger the `create-embedding-for-post` task.
<Info>
This guide covers how to setup an API endpoint using the Next.js App Router. You can find examples
for Next.js Server Actions and Pages Router in the [Trigger.dev
documentation](https://trigger.dev/docs/guides/frameworks/nextjs).
</Info>
<Steps titleSize="h3">
<Step title="Create a route handler">
Add a route handler by creating a new `route.ts` file in a `/app/api/create-embedding-for-post` directory:
```ts app/api/create-embedding-for-post/route.ts
import type { createEmbeddingForPost } from "@/trigger/create-embedding-for-post";
import { tasks } from "@trigger.dev/sdk";
import { NextResponse } from "next/server";
export async function POST(req: Request) {
const authHeader = req.headers.get("authorization");
if (!authHeader || authHeader !== `Bearer ${process.env.SEQUIN_WEBHOOK_SECRET}`) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const payload = await req.json();
const handle = await tasks.trigger<typeof createEmbeddingForPost>(
"create-embedding-for-post",
payload
);
return NextResponse.json(handle);
}
```
This route handler will receive records from Sequin, parse them, and then trigger the `create-embedding-for-post` task.
</Step>
<Step title="Set secret keys">
You'll need to set four secret keys in a `.env.local` file:
```bash
SEQUIN_WEBHOOK_SECRET=your-secret-key
TRIGGER_SECRET_KEY=secret-from-trigger-dev
OPENAI_API_KEY=sk-proj-asdfasdfasdf
DATABASE_URL=postgresql://
```
The `SEQUIN_WEBHOOK_SECRET` ensures that only Sequin can access your API endpoint.
The `TRIGGER_SECRET_KEY` is used to authenticate requests to Trigger.dev and can be found in the **API keys** tab of the Trigger.dev dashboard.
The `OPENAI_API_KEY` and `DATABASE_URL` are used to create an embedding using OpenAI and connect to your database. Be sure to add these as [environment variables](https://trigger.dev/docs/deploy-environment-variables) in Trigger.dev as well.
</Step>
</Steps>
<Check>
You've successfully created an API endpoint that can receive record payloads from Sequin and
trigger a Trigger.dev task. In the next step, you'll setup Sequin to trigger the endpoint.
</Check>
## Create Sequin consumer
You'll now configure Sequin to send every row in your `posts` table to your Trigger.dev task.
<Steps titleSize="h3">
<Step title="Connect Sequin to your database">
1. Login to your Sequin account and click the **Add New Database** button.
2. Enter the connection details for your Postgres database.
<Info>
If you need to connect to a local dev database, flip the **use localhost** switch and follow the instructions to create a tunnel using the [Sequin CLI](https://sequinstream.com/docs/cli).
</Info>
3. Follow the instructions to create a publication and a replication slot by running two SQL commands in your database:
```sql
create publication sequin_pub for all tables;
select pg_create_logical_replication_slot('sequin_slot', 'pgoutput');
```
4. Name your database and click the **Connect Database** button.
Sequin will connect to your database and ensure that it's configured properly.
<Note>
If you need step-by-step connection instructions to connect Sequin to your database, check out our [quickstart guide](https://sequinstream.com/docs/quickstart).
</Note>
</Step>
<Step title="Tunnel to your local endpoint">
Now, create a tunnel to your local endpoint so Sequin can deliver change payloads to your local API:
1. In the Sequin console, open the **HTTP Endpoint** tab and click the **Create HTTP Endpoint** button.
2. Enter a name for your endpoint (i.e. `local_endpoint`) and flip the **Use localhost** switch. Follow the instructions in the Sequin console to [install the Sequin CLI](https://sequinstream.com/docs/cli), then run:
```bash
sequin tunnel --ports=3001:local_endpoint
```
3. Now, click **Add encryption header** and set the key to `Authorization` and the value to `Bearer SEQUIN_WEBHOOK_SECRET`.
4. Click **Create HTTP Endpoint**.
</Step>
<Step title="Create a Push Consumer">
Create a push consumer that will capture posts from your database and deliver them to your local endpoint:
1. Navigate to the **Consumers** tab and click the **Create Consumer** button.
2. Select your `posts` table (i.e `public.posts`).
3. You want to ensure that every post receives an embedding - and that embeddings are updated as posts are updated. To do this, select to process **Rows** and click **Continue**.
<Note>
You can also use **changes** for this particular use case, but **rows** comes with some nice replay and backfill features.
</Note>
4. You'll now set the sort and filter for the consumer. For this guide, we'll sort by `updated_at` and start at the beginning of the table. We won't apply any filters:
<Frame>
<img src="/images/sequin-sort-and-filter.png" alt="Consumer Sort and Filter" />
</Frame>
5. On the next screen, select **Push** to have Sequin send the events to your webhook URL. Click **Continue**.
6. Now, give your consumer a name (i.e. `posts_push_consumer`) and in the **HTTP Endpoint** section select the `local_endpoint` you created above. Add the exact API route you created in the previous step (i.e. `/api/create-embedding-for-post`):
<Frame>
<img src="/images/sequin-consumer-config.png" alt="Consumer Endpoint" />
</Frame>
7. Click the **Create Consumer** button.
</Step>
</Steps>
<Check>Your Sequin consumer is now created and ready to send events to your API endpoint.</Check>
## Test end-to-end
<Steps titleSize="h3">
<Step title="Spin up you dev environment">
1. The Next.js app is running: `npm run dev`
2. The Trigger.dev dev server is running `npx trigger.dev@latest dev`
3. The Sequin tunnel is running: `sequin tunnel --ports=3001:local_endpoint`
</Step>
<Step title="Create a new post in your database">
```sql
insert into
posts (title, body, author)
values
(
'The Future of AI',
'An insightful look into how artificial intelligence is shaping the future of technology and society.',
'Alice H Johnson'
);
```
</Step>
<Step title="Trace the change in the Sequin dashboard">
In the Sequin console, navigate to the [**Trace**](https://console.sequinstream.com/trace) tab and confirm that Sequin delivered the event to your local endpoint:
<Frame>
<img src="/images/sequin-trace.png" alt="Trace Event" />
</Frame>
</Step>
<Step title="Confirm the event was received by your endpoint">
In your local terminal, you should see a `200` response in your Next.js app:
```bash
POST /api/create-embedding-for-post 200 in 262ms
```
</Step>
<Step title="Observe the task run in the Trigger.dev dashboard">
Finally, in the [**Trigger.dev dashboard**](https://cloud.trigger.dev/), navigate to the Runs page and confirm that the task run completed successfully:
<Frame>
<img src="/images/sequin-final-run.png" alt="Task run" />
</Frame>
</Step>
</Steps>
<Check>
Every time a post is created or updated, Sequin will deliver the row payload to your API endpoint
and Trigger.dev will run the `create-embedding-for-post` task.
</Check>
## Next steps
With Sequin and Trigger.dev, every post in your database will now have an embedding. This is a simple example of how you can trigger long-running tasks on database changes.
From here, add error handling and deploy to production:
- Add [retries](/errors-retrying) to your Trigger.dev task to ensure that any errors are captured and logged.
- Deploy to [production](/guides/frameworks/nextjs#deploying-your-task-to-trigger-dev) and update your Sequin consumer to point to your production database and endpoint.
@@ -0,0 +1,75 @@
---
title: "Authenticating Supabase tasks: JWTs and service roles"
sidebarTitle: "Supabase authentication"
description: "Learn how to authenticate Supabase tasks using JWTs for Row Level Security (RLS) or service role keys for admin access."
---
import SupabaseDocsCards from "/snippets/supabase-docs-cards.mdx";
There are two ways to authenticate your Supabase client in Trigger.dev tasks:
### 1. Using JWT Authentication (Recommended for User-Specific Operations)
A JWT (JSON Web Token) is a string-formatted data container that typically stores user identity and permissions data. Row Level Security policies are based on the information present in JWTs. Supabase JWT docs can be found [here](https://supabase.com/docs/guides/auth/jwts).
To use JWTs with Supabase, you'll need to add the `SUPABASE_JWT_SECRET` environment variable in your project. This secret is used to sign the JWTs. This can be found in your Supabase project settings under `Data API`.
This example code shows how to create a JWT token for a user and initialize a Supabase client with that token for authentication, allowing the task to perform database operations as that specific user. You can adapt this code to fit your own use case.
```ts
// The rest of your task code
async run(payload: { user_id: string }) {
const { user_id } = payload;
// Optional error handling
const jwtSecret = process.env.SUPABASE_JWT_SECRET;
if (!jwtSecret) {
throw new Error(
"SUPABASE_JWT_SECRET is not defined in environment variables"
);
}
// Create a JWT token for the user that expires in 1 hour
const token = jwt.sign({ sub: user_id }, jwtSecret, { expiresIn: "1h" });
// Initialize the Supabase client with the JWT token
const supabase = createClient(
// These details can be found in your Supabase project settings under `Data API`
process.env.SUPABASE_URL as string,
process.env.SUPABASE_ANON_KEY as string,
{
global: {
headers: {
Authorization: `Bearer ${token}`,
},
},
}
);
// The rest of your task code
```
Using JWTs to authenticate Supabase operations is more secure than using service role keys because it respects Row Level Security policies, maintains user-specific audit trails, and follows the principle of least privileged access.
### 2. Using Service Role Key (For Admin-Level Access)
<Warning>
The service role key has unlimited access and bypasses all security checks. Only use it when you
need admin-level privileges, and never expose it client-side.
</Warning>
This example code creates a Supabase client with admin-level privileges using a service role key, bypassing all Row Level Security policies to allow unrestricted database access.
```ts
// Create a single Supabase client for interacting with your database
// 'Database' supplies the type definitions to supabase-js
const supabase = createClient<Database>(
// These details can be found in your Supabase project settings under `API`
process.env.SUPABASE_PROJECT_URL as string, // e.g. https://abc123.supabase.co - replace 'abc123' with your project ID
process.env.SUPABASE_SERVICE_ROLE_KEY as string // Your service role secret key
);
// Your task
```
<SupabaseDocsCards />
@@ -0,0 +1,215 @@
---
title: "Triggering tasks from Supabase edge functions"
sidebarTitle: "Edge function hello world"
description: "This guide will show you how to trigger a task from a Supabase edge function, and then view the run in our dashboard."
---
import Prerequisites from "/snippets/framework-prerequisites.mdx";
import SupabasePrerequisites from "/snippets/supabase-prerequisites.mdx";
import CliInitStep from "/snippets/step-cli-init.mdx";
import CliDevStep from "/snippets/step-cli-dev.mdx";
import CliRunTestStep from "/snippets/step-run-test.mdx";
import CliViewRunStep from "/snippets/step-view-run.mdx";
import UsefulNextSteps from "/snippets/useful-next-steps.mdx";
import TriggerTaskNextjs from "/snippets/trigger-tasks-nextjs.mdx";
import NextjsTroubleshootingMissingApiKey from "/snippets/nextjs-missing-api-key.mdx";
import NextjsTroubleshootingButtonSyntax from "/snippets/nextjs-button-syntax.mdx";
import SupabaseDocsCards from "/snippets/supabase-docs-cards.mdx";
import SupabaseAuthInfo from "/snippets/supabase-auth-info.mdx";
## Overview
Supabase edge functions allow you to trigger tasks either when an event is sent from a third party (e.g. when a new Stripe payment is processed, when a new user signs up to a service, etc), or when there are any changes or updates to your Supabase database.
This guide shows you how to set up and deploy a simple Supabase edge function example that triggers a task when an edge function URL is accessed.
## Prerequisites
- Ensure you have the [Supabase CLI](https://supabase.com/docs/guides/cli/getting-started) installed
- Since Supabase CLI version 1.123.4, you must have [Docker Desktop installed](https://supabase.com/docs/guides/functions/deploy#deploy-your-edge-functions) to deploy Edge Functions
- Ensure TypeScript is installed
- [Create a Trigger.dev account](https://cloud.trigger.dev)
- Create a new Trigger.dev project
## GitHub repo
<Card
title="View the project on GitHub"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/supabase-edge-functions"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## Initial setup
<Steps>
<SupabasePrerequisites />
<CliInitStep />
<CliDevStep />
<CliRunTestStep />
<CliViewRunStep />
</Steps>
## Create a new Supabase edge function and deploy it
<Steps>
<Step title="Create a new Supabase edge function">
We'll call this example `edge-function-trigger`.
In your project, run the following command in the terminal using the Supabase CLI:
```bash
supabase functions new edge-function-trigger
```
</Step>
<Step title="Update the edge function code">
Replace the placeholder code in your `edge-function-trigger/index.ts` file with the following:
```ts functions/edge-function-trigger/index.ts
// Setup type definitions for built-in Supabase Runtime APIs
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
// Import the Trigger.dev SDK - replace "<your-sdk-version>" with the version of the SDK you are using, e.g. "3.0.0". You can find this in your package.json file.
import { tasks } from "npm:@trigger.dev/sdk@3.0.0";
// Import your task type from your /trigger folder
import type { helloWorldTask } from "../../../src/trigger/example.ts";
// 👆 **type-only** import
Deno.serve(async () => {
await tasks.trigger<typeof helloWorldTask>(
// Your task id
"hello-world",
// Your task payload
"Hello from a Supabase Edge Function!"
);
return new Response("OK");
});
```
<Note>You can only import the `type` from the task.</Note>
<Note>
Tasks in the `trigger` folder use Node, so they must stay in there or they will not run,
especially if you are using a different runtime like Deno. Also do not add "`npm:`" to imports
inside your task files, for the same reason.
</Note>
</Step>
<Step title="Deploy your edge function using the Supabase CLI">
You can now deploy your edge function with the following command in your terminal:
```bash
supabase functions deploy edge-function-trigger --no-verify-jwt
```
<Warning>
`--no-verify-jwt` removes the JSON Web Tokens requirement from the authorization header. By
default this should be on, but it is not strictly required for this hello world example.
</Warning>
<SupabaseAuthInfo />
Follow the CLI instructions and once complete you should now see your new edge function deployment in your Supabase edge functions dashboard.
There will be a link to the dashboard in your terminal output, or you can find it at this URL:
`https://supabase.com/dashboard/project/<your-project-id>/functions`
<Note>Replace `your-project-id` with your actual project ID.</Note>
</Step>
</Steps>
## Set your Trigger.dev prod secret key in the Supabase dashboard
To trigger a task from your edge function, you need to set your Trigger.dev secret key in the Supabase dashboard.
To do this, first go to your Trigger.dev [project dashboard](https://cloud.trigger.dev) and copy the `prod` secret key from the API keys page.
![How to find your prod secret key](/images/api-key-prod.png)
Then, in [Supabase](https://supabase.com/dashboard/projects), select your project, navigate to 'Project settings' <Icon icon="circle-1" iconType="solid" size={20} color="A8FF53" />, click 'Edge functions' <Icon icon="circle-2" iconType="solid" size={20} color="A8FF53" /> in the configurations menu, and then click the 'Add new secret' <Icon icon="circle-3" iconType="solid" size={20} color="A8FF53" /> button.
Add `TRIGGER_SECRET_KEY` <Icon icon="circle-4" iconType="solid" size={20} color="A8FF53" /> with the pasted value of your Trigger.dev `prod` secret key.
![Add secret key in Supabase](/images/supabase-keys-1.png)
## Deploy your task and trigger it from your edge function
<Steps>
<Step title="Deploy your 'Hello World' task">
Next, deploy your `hello-world` task to [Trigger.dev cloud](https://cloud.trigger.dev).
<CodeGroup>
```bash npm
npx trigger.dev@latest deploy
```
```bash pnpm
pnpm dlx trigger.dev@latest deploy
```
```bash yarn
yarn dlx trigger.dev@latest deploy
```
</CodeGroup>
</Step>
<Step title="Trigger a prod run from your deployed edge function">
To do this all you need to do is simply open the `edge-function-trigger` URL.
`https://supabase.com/dashboard/project/<your-project-id>/functions`
<Note>Replace `your-project-id` with your actual project ID.</Note>
In your Supabase project, go to your Edge function dashboard, find `edge-function-trigger`, copy the URL, and paste it into a new window in your browser.
Once loaded you should see OK on the new screen.
![Edge function URL](/images/supabase-function-url.png)
The task will be triggered when your edge function URL is accessed.
Check your [cloud.trigger.dev](https://cloud.trigger.dev) dashboard and you should see a successful `hello-world` task.
**Congratulations, you have run a simple Hello World task from a Supabase edge function!**
</Step>
</Steps>
### If you see a runtime error when calling tasks.trigger()
If you see `TypeError: Cannot read properties of undefined (reading 'toString')` when calling `tasks.trigger()` from your edge function, the SDK is hitting a dependency that expects Node-style APIs not available in the Supabase Edge (Deno) runtime. Use the [Tasks API](/management/tasks/trigger) with `fetch` instead of the SDK—that avoids loading the SDK in Deno:
```ts
const response = await fetch(
`https://api.trigger.dev/api/v1/tasks/your-task-id/trigger`,
{
method: "POST",
headers: {
Authorization: `Bearer ${Deno.env.get("TRIGGER_SECRET_KEY")}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ payload: { your: "payload" } }),
}
);
```
See [Trigger task via API](/management/tasks/trigger) for full request/response details and optional fields (e.g. `delay`, `idempotencyKey`).
<SupabaseDocsCards />
@@ -0,0 +1,435 @@
---
title: "Triggering tasks from Supabase Database Webhooks"
sidebarTitle: "Database webhooks"
description: "This guide shows you how to trigger a transcribing task when a row is added to a table in a Supabase database, using a Database Webhook and Edge Function."
---
import Prerequisites from "/snippets/framework-prerequisites.mdx";
import SupabasePrerequisites from "/snippets/supabase-prerequisites.mdx";
import UsefulNextSteps from "/snippets/useful-next-steps.mdx";
import TriggerTaskNextjs from "/snippets/trigger-tasks-nextjs.mdx";
import NextjsTroubleshootingMissingApiKey from "/snippets/nextjs-missing-api-key.mdx";
import NextjsTroubleshootingButtonSyntax from "/snippets/nextjs-button-syntax.mdx";
import SupabaseDocsCards from "/snippets/supabase-docs-cards.mdx";
import SupabaseAuthInfo from "/snippets/supabase-auth-info.mdx";
## Overview
Supabase and Trigger.dev can be used together to create powerful workflows triggered by real-time changes in your database tables:
- A Supabase Database Webhook triggers an Edge Function when a row including a video URL is inserted into a table
- The Edge Function triggers a Trigger.dev task, passing the `video_url` column data from the new table row as the payload
- The Trigger.dev task then:
- Uses [FFmpeg](https://www.ffmpeg.org/) to extract the audio track from a video URL
- Uses [Deepgram](https://deepgram.com) to transcribe the extracted audio
- Updates the original table row using the `record.id` in Supabase with the new transcription using `update`
## Prerequisites
- Ensure you have the [Supabase CLI](https://supabase.com/docs/guides/cli/getting-started) installed
- Since Supabase CLI version 1.123.4, you must have [Docker Desktop installed](https://supabase.com/docs/guides/functions/deploy#deploy-your-edge-functions) to deploy Edge Functions
- Ensure TypeScript is installed
- [Create a Trigger.dev account](https://cloud.trigger.dev)
- Create a new Trigger.dev project
- [Create a new Deepgram account](https://deepgram.com/) and get your API key from the dashboard
## GitHub repo
<Card
title="View the project on GitHub"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/supabase-edge-functions"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## Initial setup
<Steps>
<SupabasePrerequisites />
<Step title="Run the CLI `init` command">
The easiest way to get started is to use the CLI. It will add Trigger.dev to your existing project, create a `/trigger` folder and give you an example task.
Run this command in the root of your project to get started:
<CodeGroup>
```bash npm
npx trigger.dev@latest init
```
```bash pnpm
pnpm dlx trigger.dev@latest init
```
```bash yarn
yarn dlx trigger.dev@latest init
```
</CodeGroup>
It will do a few things:
1. Log you into the CLI if you're not already logged in.
2. Create a `trigger.config.ts` file in the root of your project.
3. Ask where you'd like to create the `/trigger` directory.
4. Create the `/trigger` directory with an example task, `/trigger/example.[ts/js]`.
Choose "None" when prompted to install an example task. We will create a new task for this guide.
</Step>
</Steps>
## Create a new table in your Supabase database
First, in the Supabase project dashboard, you'll need to create a new table to store the video URL and transcription.
To do this, click on 'Table Editor' <Icon icon="circle-1" iconType="solid" size={20} color="A8FF53" /> in the left-hand menu and create a new table. <Icon icon="circle-2" iconType="solid" size={20} color="A8FF53" />
![How to create a new Supabase table](/images/supabase-new-table-1.png)
Call your table `video_transcriptions`. <Icon icon="circle-1" iconType="solid" size={20} color="A8FF53" />
Add two new columns, one called `video_url` with the type `text` <Icon icon="circle-2" iconType="solid" size={20} color="A8FF53" />, and another called `transcription`, also with the type `text` <Icon icon="circle-3" iconType="solid" size={20} color="A8FF53" />.
![How to create a new Supabase table 2](/images/supabase-new-table-2.png)
## Create and deploy the Trigger.dev task
### Generate the Database type definitions
To allow you to use TypeScript to interact with your table, you need to [generate the type definitions](https://supabase.com/docs/guides/api/rest/generating-types) for your Supabase table using the Supabase CLI.
```bash
supabase gen types --lang=typescript --project-id <project-ref> --schema public > database.types.ts
```
<Note> Replace `<project-ref>` with your Supabase project reference ID. This can be found in your Supabase project settings under 'General'. </Note>
### Create the transcription task
Create a new task file in your `/trigger` folder. Call it `videoProcessAndUpdate.ts`.
This task takes a video from a public video url, extracts the audio using FFmpeg and transcribes the audio using Deepgram. The transcription summary will then be updated back to the original row in the `video_transcriptions` table in Supabase.
You will need to install some additional dependencies for this task:
<CodeGroup>
```bash npm
npm install @deepgram/sdk @supabase/supabase-js fluent-ffmpeg
```
```bash pnpm
pnpm install @deepgram/sdk @supabase/supabase-js fluent-ffmpeg
```
```bash yarn
yarn install @deepgram/sdk @supabase/supabase-js fluent-ffmpeg
```
</CodeGroup>
These dependencies will allow you to interact with the Deepgram and Supabase APIs and extract audio from a video using FFmpeg.
<Warning>
When updating your tables from a Trigger.dev task which has been triggered by a database change,
be extremely careful to not cause an infinite loop. Ensure you have the correct conditions in
place to prevent this.
</Warning>
```ts /trigger/videoProcessAndUpdate.ts
// Install any missing dependencies below
import { createClient as createDeepgramClient } from "@deepgram/sdk";
import { createClient as createSupabaseClient } from "@supabase/supabase-js";
import { logger, task } from "@trigger.dev/sdk";
import ffmpeg from "fluent-ffmpeg";
import fs from "fs";
import { Readable } from "node:stream";
import os from "os";
import path from "path";
import { Database } from "../../database.types";
// Create a single Supabase client for interacting with your database
// 'Database' supplies the type definitions to supabase-js
const supabase = createSupabaseClient<Database>(
// These details can be found in your Supabase project settings under `API`
process.env.SUPABASE_PROJECT_URL as string, // e.g. https://abc123.supabase.co - replace 'abc123' with your project ID
process.env.SUPABASE_SERVICE_ROLE_KEY as string // Your service role secret key
);
// Your DEEPGRAM_SECRET_KEY can be found in your Deepgram dashboard
const deepgram = createDeepgramClient(process.env.DEEPGRAM_SECRET_KEY);
export const videoProcessAndUpdate = task({
id: "video-process-and-update",
run: async (payload: { videoUrl: string; id: number }) => {
const { videoUrl, id } = payload;
logger.log(`Processing video at URL: ${videoUrl}`);
// Generate temporary file names
const tempDirectory = os.tmpdir();
const outputPath = path.join(tempDirectory, `audio_${Date.now()}.wav`);
const response = await fetch(videoUrl);
// Extract the audio using FFmpeg
await new Promise((resolve, reject) => {
if (!response.body) {
return reject(new Error("Failed to fetch video"));
}
ffmpeg(Readable.from(response.body))
.outputOptions([
"-vn", // Disable video output
"-acodec pcm_s16le", // Use PCM 16-bit little-endian encoding
"-ar 44100", // Set audio sample rate to 44.1 kHz
"-ac 2", // Set audio channels to stereo
])
.output(outputPath)
.on("end", resolve)
.on("error", reject)
.run();
});
logger.log(`Audio extracted from video`, { outputPath });
// Transcribe the audio using Deepgram
const { result, error } = await deepgram.listen.prerecorded.transcribeFile(
fs.readFileSync(outputPath),
{
model: "nova-2", // Use the Nova 2 model
smart_format: true, // Automatically format the transcription
diarize: true, // Enable speaker diarization
}
);
if (error) {
throw error;
}
const transcription = result.results.channels[0].alternatives[0].paragraphs?.transcript;
logger.log(`Transcription: ${transcription}`);
// Delete the temporary audio file
fs.unlinkSync(outputPath);
logger.log(`Temporary audio file deleted`, { outputPath });
const { error: updateError } = await supabase
.from("video_transcriptions")
// Update the transcription column
.update({ transcription: transcription })
// Find the row by its ID
.eq("id", id);
if (updateError) {
throw new Error(`Failed to update transcription: ${updateError.message}`);
}
return {
message: `Summary of the audio: ${transcription}`,
result,
};
},
});
```
<Warning>
This task uses your service role secret key to bypass Row Level Security. This is not recommended
for production use as it has unlimited access and bypasses all security checks.
</Warning>
<SupabaseAuthInfo />
### Adding the FFmpeg build extension
Before you can deploy the task, you'll need to add the FFmpeg build extension to your `trigger.config.ts` file.
```ts trigger.config.ts
// Add this import
import { ffmpeg } from "@trigger.dev/build/extensions/core";
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
project: "<project ref>", // Replace with your project ref
// Your other config settings...
build: {
// Add the FFmpeg build extension
extensions: [ffmpeg()],
},
});
```
<Note>
[Build extensions](/config/extensions/overview) allow you to hook into the build system and
customize the build process or the resulting bundle and container image (in the case of
deploying). You can use pre-built extensions or create your own.
</Note>
<Note>
You'll also need to add `@trigger.dev/build` to your `package.json` file under `devDependencies`
if you don't already have it there.
</Note>
If you are modifying this example and using popular FFmpeg libraries like `fluent-ffmpeg` you'll also need to add them to [`external`](/config/config-file#external) in your `trigger.config.ts` file.
### Add your Deepgram and Supabase environment variables to your Trigger.dev project
You will need to add your `DEEPGRAM_SECRET_KEY`, `SUPABASE_PROJECT_URL` and `SUPABASE_SERVICE_ROLE_KEY` as environment variables in your Trigger.dev project. This can be done in the 'Environment Variables' page in your project dashboard.
![Adding environment variables](/images/environment-variables-page.jpg)
### Deploying your task
Now you can now deploy your task using the following command:
<CodeGroup>
```bash npm
npx trigger.dev@latest deploy
```
```bash pnpm
pnpm dlx trigger.dev@latest deploy
```
```bash yarn
yarn dlx trigger.dev@latest deploy
```
</CodeGroup>
## Create and deploy the Supabase Edge Function
### Add your Trigger.dev prod secret key to the Supabase dashboard
Go to your Trigger.dev [project dashboard](https://cloud.trigger.dev) and copy the `prod` secret key from the API keys page.
![How to find your prod secret key](/images/api-key-prod.png)
Then, in [Supabase](https://supabase.com/dashboard/projects), select the project you want to use, navigate to 'Project settings' <Icon icon="circle-1" iconType="solid" size={20} color="A8FF53" />, click 'Edge Functions' <Icon icon="circle-2" iconType="solid" size={20} color="A8FF53" /> in the configurations menu, and then click the 'Add new secret' <Icon icon="circle-3" iconType="solid" size={20} color="A8FF53" /> button.
Add `TRIGGER_SECRET_KEY` <Icon icon="circle-4" iconType="solid" size={20} color="A8FF53" /> with the pasted value of your Trigger.dev `prod` secret key.
![Add secret key in Supabase](/images/supabase-keys-1.png)
### Create a new Edge Function using the Supabase CLI
Now create an Edge Function using the Supabase CLI. Call it `video-processing-handler`. This function will be triggered by the Database Webhook.
```bash
supabase functions new video-processing-handler
```
```ts functions/video-processing-handler/index.ts
// Setup type definitions for built-in Supabase Runtime APIs
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { tasks } from "npm:@trigger.dev/sdk@latest";
// Import the videoProcessAndUpdate task from the trigger folder
import type { videoProcessAndUpdate } from "../../../src/trigger/videoProcessAndUpdate.ts";
// 👆 type only import
// Sets up a Deno server that listens for incoming JSON requests
Deno.serve(async (req) => {
const payload = await req.json();
// This payload will contain the video url and id from the new row in the table
const videoUrl = payload.record.video_url;
const id = payload.record.id;
// Trigger the videoProcessAndUpdate task with the videoUrl payload
await tasks.trigger<typeof videoProcessAndUpdate>("video-process-and-update", { videoUrl, id });
console.log(payload ?? "No name provided");
return new Response("ok");
});
```
<Note>
Tasks in the `trigger` folder use Node, so they must stay in there or they will not run,
especially if you are using a different runtime like Deno. Also do not add "`npm:`" to imports
inside your task files, for the same reason.
</Note>
### Deploy the Edge Function
Now deploy your new Edge Function with the following command:
```bash
supabase functions deploy video-processing-handler
```
Follow the CLI instructions, selecting the same project you added your `prod` secret key to, and once complete you should see your new Edge Function deployment in your Supabase Edge Functions dashboard.
There will be a link to the dashboard in your terminal output.
## Create the Database Webhook
In your Supabase project dashboard, click 'Project settings' <Icon icon="circle-1" iconType="solid" size={20} color="A8FF53" />, then the 'API' tab <Icon icon="circle-2" iconType="solid" size={20} color="A8FF53" />, and copy the `anon` `public` API key from the table <Icon icon="circle-3" iconType="solid" size={20} color="A8FF53" />.
![How to find your Supabase API keys](/images/supabase-api-key.png)
Then, go to 'Database' <Icon icon="circle-1" iconType="solid" size={20} color="A8FF53" /> click on 'Webhooks' <Icon icon="circle-2" iconType="solid" size={20} color="A8FF53" />, and then click 'Create a new hook' <Icon icon="circle-3" iconType="solid" size={20} color="A8FF53" />.
![How to create a new webhook](/images/supabase-create-webhook-1.png)
<Icon icon="circle-1" iconType="solid" size={20} color="A8FF53" /> Call the hook `edge-function-hook`.
<Icon icon="circle-2" iconType="solid" size={20} color="A8FF53" /> Select the new table you have created:
`public` `video_transcriptions`.
<Icon icon="circle-3" iconType="solid" size={20} color="A8FF53" /> Choose the `insert` event.
![How to create a new webhook 2](/images/supabase-create-webhook-2.png)
<Icon icon="circle-4" iconType="solid" size={20} color="A8FF53" /> Under 'Webhook configuration', select
'Supabase Edge Functions'{" "}
<Icon icon="circle-5" iconType="solid" size={20} color="A8FF53" /> Under 'Edge Function', choose `POST`
and select the Edge Function you have created: `video-processing-handler`.{" "}
<Icon icon="circle-6" iconType="solid" size={20} color="A8FF53" /> Under 'HTTP Headers', add a new header with the key `Authorization` and the value `Bearer <your-api-key>` (replace `<your-api-key>` with the `anon` `public` API key you copied earlier).
<Info>
Supabase Edge Functions require a JSON Web Token [JWT](https://supabase.com/docs/guides/auth/jwts)
in the authorization header. This is to ensure that only authorized users can access your edge
functions.
</Info>
<Icon icon="circle-7" iconType="solid" size={20} color="A8FF53" /> Click 'Create webhook'.{" "}
![How to create a new webhook 3](/images/supabase-create-webhook-3.png)
Your Database Webhook is now ready to use.
## Triggering the entire workflow
Your `video-processing-handler` Edge Function is now set up to trigger the `videoProcessAndUpdate` task every time a new row is inserted into your `video_transcriptions` table.
To do this, go back to your Supabase project dashboard, click on 'Table Editor' <Icon icon="circle-1" iconType="solid" size={20} color="A8FF53" /> in the left-hand menu, click on the `video_transcriptions` table <Icon icon="circle-2" iconType="solid" size={20} color="A8FF53" /> , and then click 'Insert', 'Insert Row' <Icon icon="circle-3" iconType="solid" size={20} color="A8FF53" />.
![How to insert a new row 1](/images/supabase-new-table-3.png)
Add a new item under `video_url`, with a public video url. <Icon icon="circle-1" iconType="solid" size={20} color="A8FF53" />.
You can use the following public video URL for testing: `https://content.trigger.dev/Supabase%20Edge%20Functions%20Quickstart.mp4`.
![How to insert a new row 2](/images/supabase-new-table-4.png)
Once the new table row has been inserted, check your [cloud.trigger.dev](https://cloud.trigger.dev) project 'Runs' list <Icon icon="circle-1" iconType="solid" size={20} color="A8FF53" /> and you should see a processing `videoProcessAndUpdate` task <Icon icon="circle-2" iconType="solid" size={20} color="A8FF53" /> which has been triggered when you added a new row with the video url to your `video_transcriptions` table.
![Supabase successful run](/images/supabase-run-result.png)
Once the run has completed successfully, go back to your Supabase `video_transcriptions` table, and you should see that in the row containing the original video URL, the transcription has now been added to the `transcription` column.
![Supabase successful table update](/images/supabase-table-result.png)
**Congratulations! You have completed the full workflow from Supabase to Trigger.dev and back again.**
<SupabaseDocsCards />
@@ -0,0 +1,9 @@
---
title: "Supabase overview"
sidebarTitle: "Overview"
description: "Guides and examples for using Supabase with Trigger.dev."
---
import SupabaseDocsCards from "/snippets/supabase-docs-cards.mdx";
<SupabaseDocsCards />
@@ -0,0 +1,45 @@
---
title: "Using webhooks with Trigger.dev"
sidebarTitle: "Overview"
description: "Guides for using webhooks with Trigger.dev."
---
## Overview
Webhooks are a way to send and receive events from external services. Triggering tasks using webhooks allow you to add real-time, event driven functionality to your app.
A webhook handler is code that executes in response to an event. They can be endpoints in your framework's routing which can be triggered by an external service.
## Webhook guides
<CardGroup cols={2}>
<Card
title="Next.js - triggering tasks using webhooks"
icon="N"
href="/guides/frameworks/nextjs-webhooks"
>
How to create a webhook handler in a Next.js app, and trigger a task from it.
</Card>
<Card
title="Remix - triggering tasks using webhooks"
icon="R"
href="/guides/frameworks/remix-webhooks"
>
How to create a webhook handler in a Remix app, and trigger a task from it.
</Card>
<Card title="Stripe webhooks" icon="webhook" href="/guides/examples/stripe-webhook">
How to create a Stripe webhook handler and trigger a task when a 'checkout session completed'
event is received.
</Card>
<Card title="Hookdeck webhooks" icon="webhook" href="/guides/examples/hookdeck-webhook">
Use Hookdeck to receive webhooks and forward them to Trigger.dev tasks with logging and replay
capabilities.
</Card>
<Card
title="Supabase database webhooks guide"
icon="webhook"
href="/guides/frameworks/supabase-edge-functions-database-webhooks"
>
Learn how to trigger a task from a Supabase edge function when an event occurs in your database.
</Card>
</CardGroup>
+106
View File
@@ -0,0 +1,106 @@
---
title: "Frameworks, guides and examples"
sidebarTitle: "Overview"
description: "A growing list of guides and examples to get the most out of Trigger.dev."
mode: "center"
---
## Frameworks
<CardGroup cols={3}>
<Card title="Bun" img="/images/logo-bun.png" href="/guides/frameworks/bun" />
<Card title="Next.js" img="/images/logo-nextjs.png" href="/guides/frameworks/nextjs" />
<Card title="Node.js" img="/images/logo-nodejs.png" href="/guides/frameworks/nodejs" />
<Card title="Remix" img="/images/logo-remix.png" href="/guides/frameworks/remix" />
<Card title="SvelteKit" img="/images/logo-svelte.png" href="/guides/community/sveltekit" />
</CardGroup>
import UseCasesCards from "/snippets/use-cases-cards.mdx";
## Guides
Get set up fast using our detailed walk-through guides.
| Guide | Description |
| :----------------------------------------------------------------------------------------- | :------------------------------------------------------------------- |
| [AI Agent: Content moderation](/guides/ai-agents/respond-and-check-content) | Parallel check content while responding to customers |
| [AI Agent: Generate and translate copy](/guides/ai-agents/generate-translate-copy) | Chain prompts to generate and translate content |
| [AI Agent: News verification](/guides/ai-agents/verify-news-article) | Orchestrate fact checking of news articles |
| [AI Agent: Route questions](/guides/ai-agents/route-question) | Route questions to different models based on complexity |
| [AI Agent: Translation refinement](/guides/ai-agents/translate-and-refine) | Evaluate and refine translations with feedback |
| [Claude Agent SDK](/guides/ai-agents/claude-code-trigger) | Build AI agents that read files, run commands, and edit code |
| [Cursor rules](/guides/cursor-rules) | Use Cursor rules to help write Trigger.dev tasks |
| [Prisma](/guides/frameworks/prisma) | How to setup Prisma with Trigger.dev |
| [Python image processing](/guides/python/python-image-processing) | Use Python and Pillow to process images |
| [Python document to markdown](/guides/python/python-doc-to-markdown) | Use Python and MarkItDown to convert documents to markdown |
| [Python PDF form extractor](/guides/python/python-pdf-form-extractor) | Use Python, PyMuPDF and Trigger.dev to extract data from a PDF form |
| [Python web crawler](/guides/python/python-crawl4ai) | Use Python, Crawl4AI and Playwright to create a headless web crawler |
| [Sequin database triggers](/guides/frameworks/sequin) | Trigger tasks from database changes using Sequin |
| [Stripe webhooks](/guides/examples/stripe-webhook) | Trigger tasks from incoming Stripe webhook events |
| [Hookdeck webhooks](/guides/examples/hookdeck-webhook) | Use Hookdeck to receive webhooks and forward them to Trigger.dev tasks |
| [Supabase database webhooks](/guides/frameworks/supabase-edge-functions-database-webhooks) | Trigger tasks using Supabase database webhooks |
| [Supabase edge function hello world](/guides/frameworks/supabase-edge-functions-basic) | Trigger tasks from Supabase edge function |
| [Using webhooks in Next.js](/guides/frameworks/nextjs-webhooks) | Trigger tasks from a webhook in Next.js |
| [Using webhooks in Remix](/guides/frameworks/remix-webhooks) | Trigger tasks from a webhook in Remix |
<UseCasesCards />
## Example projects
Example projects are full projects with example repos you can fork and use. These are a great way of learning how to use Trigger.dev in your projects.
| Example project | Description | Framework | GitHub |
| :-------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------- | :-------- | :------------------------------------------------------------------------------------------------------------- |
| [Anchor Browser web scraper](/guides/example-projects/anchor-browser-web-scraper) | Monitor a website and find the cheapest tickets for a show. | — | [View the repo](https://github.com/triggerdotdev/examples/tree/main/anchor-browser-web-scraper) |
| [Batch LLM Evaluator](/guides/example-projects/batch-llm-evaluator) | Evaluate multiple LLM models and stream the results to the frontend. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/batch-llm-evaluator) |
| [Claude changelog generator](/guides/example-projects/claude-changelog-generator) | Automatically generate professional changelogs from git commits using Claude. | — | [View the repo](https://github.com/triggerdotdev/examples/tree/main/changelog-generator) |
| [Claude GitHub wiki agent](/guides/example-projects/claude-github-wiki) | Generate and maintain GitHub wiki documentation with Claude-powered analysis. | — | [View the repo](https://github.com/triggerdotdev/examples/tree/main/claude-agent-github-wiki) |
| [Claude thinking chatbot](/guides/example-projects/claude-thinking-chatbot) | Use Vercel's AI SDK and Anthropic's Claude 3.7 model to create a thinking chatbot. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/claude-thinking-chatbot) |
| [Cursor background agent](/guides/example-projects/cursor-background-agent) | Run Cursor's headless CLI agent as a background task, streaming live output to the browser. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/cursor-cli-demo) |
| [Human-in-the-loop workflow](/guides/example-projects/human-in-the-loop-workflow) | Create audio summaries of newspaper articles using a human-in-the-loop workflow built with ReactFlow and Trigger.dev waitpoint tokens. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/article-summary-workflow) |
| [Mastra agents with memory](/guides/example-projects/mastra-agents-with-memory) | Use Mastra to create a weather agent that can collect live weather data and generate clothing recommendations. | — | [View the repo](https://github.com/triggerdotdev/examples/tree/main/mastra-agents) |
| [OpenAI Agents SDK for Python guardrails](/guides/example-projects/openai-agent-sdk-guardrails) | Use the OpenAI Agents SDK for Python to create a guardrails system for your AI agents. | — | [View the repo](https://github.com/triggerdotdev/examples/tree/main/openai-agent-sdk-guardrails-examples) |
| [OpenAI Agents SDK for TypeScript playground](/guides/example-projects/openai-agents-sdk-typescript-playground) | A playground containing 7 AI agents using the OpenAI Agents SDK for TypeScript with Trigger.dev. | — | [View the repo](https://github.com/triggerdotdev/examples/tree/main/openai-agents-sdk-with-trigger-playground) |
| [Product image generator](/guides/example-projects/product-image-generator) | Transform basic product photos into professional marketing shots using Replicate's image generation models. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/product-image-generator) |
| [Python web crawler](/guides/python/python-crawl4ai) | Use Python, Crawl4AI and Playwright to create a headless web crawler with Trigger.dev. | — | [View the repo](https://github.com/triggerdotdev/examples/tree/main/python-crawl4ai) |
| [Realtime CSV Importer](/guides/example-projects/realtime-csv-importer) | Upload a CSV file and see the progress of the task streamed to the frontend. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/realtime-csv-importer) |
| [Realtime Fal.ai image generation](/guides/example-projects/realtime-fal-ai) | Generate an image from a prompt using Fal.ai and show the progress of the task on the frontend using Realtime. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/realtime-fal-ai-image-generation) |
| [Smart Spreadsheet](/guides/example-projects/smart-spreadsheet) | Enrich company data using Exa search and Claude with real-time streaming results. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/smart-spreadsheet) |
| [Turborepo monorepo with Prisma](/guides/example-projects/turborepo-monorepo-prisma) | Use Prisma in a Turborepo monorepo with Trigger.dev. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-package) |
| [Vercel AI SDK image generator](/guides/example-projects/vercel-ai-sdk-image-generator) | Use the Vercel AI SDK to generate images from a prompt. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/vercel-ai-sdk-image-generator) |
| [Vercel AI SDK deep research agent](/guides/example-projects/vercel-ai-sdk-deep-research) | Use the Vercel AI SDK to generate comprehensive PDF reports using a deep research agent. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/vercel-ai-sdk-deep-research-agent) |
## Example tasks
Task code you can copy and paste to use in your project. They can all be extended and customized to fit your needs.
| Example task | Description |
| :---------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- |
| [DALL·E 3 image generation](/guides/examples/dall-e3-generate-image) | Use OpenAI's GPT-4o and DALL·E 3 to generate an image and text. |
| [Deepgram audio transcription](/guides/examples/deepgram-transcribe-audio) | Transcribe audio using Deepgram's speech recognition API. |
| [Fal.ai image to cartoon](/guides/examples/fal-ai-image-to-cartoon) | Convert an image to a cartoon using Fal.ai, and upload the result to Cloudflare R2. |
| [Fal.ai with Realtime](/guides/examples/fal-ai-realtime) | Generate an image from a prompt using Fal.ai and show the progress of the task on the frontend using Realtime. |
| [FFmpeg video processing](/guides/examples/ffmpeg-video-processing) | Use FFmpeg to process a video in various ways and save it to Cloudflare R2. |
| [Firecrawl URL crawl](/guides/examples/firecrawl-url-crawl) | Learn how to use Firecrawl to crawl a URL and return LLM-ready markdown. |
| [LibreOffice PDF conversion](/guides/examples/libreoffice-pdf-conversion) | Convert a document to PDF using LibreOffice. |
| [Lightpanda](/guides/examples/lightpanda) | Use Lightpanda browser (or cloud version) to get a webpage's content. |
| [OpenAI with retrying](/guides/examples/open-ai-with-retrying) | Create a reusable OpenAI task with custom retry options. |
| [PDF to image](/guides/examples/pdf-to-image) | Use `MuPDF` to turn a PDF into images and save them to Cloudflare R2. |
| [Puppeteer](/guides/examples/puppeteer) | Use Puppeteer to generate a PDF or scrape a webpage. |
| [React email](/guides/examples/react-email) | Send an email using React Email. |
| [React to PDF](/guides/examples/react-pdf) | Use `react-pdf` to generate a PDF and save it to Cloudflare R2. |
| [Resend email sequence](/guides/examples/resend-email-sequence) | Send a sequence of emails over several days using Resend with Trigger.dev. |
| [Replicate image generation](/guides/examples/replicate-image-generation) | Learn how to generate images from source image URLs using Replicate and Trigger.dev. |
| [Satori](/guides/examples/satori) | Generate OG images using React Satori. |
| [Scrape Hacker News](/guides/examples/scrape-hacker-news) | Scrape Hacker News using BrowserBase and Puppeteer, summarize the articles with ChatGPT and send an email of the summary every weekday using Resend. |
| [Sentry error tracking](/guides/examples/sentry-error-tracking) | Automatically send errors to Sentry from your tasks. |
| [Sharp image processing](/guides/examples/sharp-image-processing) | Use Sharp to process an image and save it to Cloudflare R2. |
| [Supabase database operations](/guides/examples/supabase-database-operations) | Run basic CRUD operations on a table in a Supabase database using Trigger.dev. |
| [Supabase Storage upload](/guides/examples/supabase-storage-upload) | Download a video from a URL and upload it to Supabase Storage using S3. |
| [Vercel AI SDK](/guides/examples/vercel-ai-sdk) | Use Vercel AI SDK to generate text using OpenAI. |
| [Vercel sync environment variables](/guides/examples/vercel-sync-env-vars) | Automatically sync environment variables from your Vercel projects to Trigger.dev. |
<Note>
If you would like to see a guide for your framework, or an example task for your use case, please
request it in our [Discord server](https://trigger.dev/discord) and we'll add it to the list.
</Note>
+438
View File
@@ -0,0 +1,438 @@
---
title: "Upgrade to new build system"
description: "How to use the new build system preview release"
---
Based on feedback and a steady flow of issues from our previous build system, we're happy to share with you a preview release of our new build system that (hopefully) fixes most of the issues you may have faced.
The main features of the new build sytem are:
- **Bundling by default**: All dependencies are bundled by default, so you no longer need to specify which dependencies to bundle. This solves a whole bunch of issues related to monorepos.
- **Build extensions**: A new way to extend the build process with custom logic. This is a more flexible and powerful way to extend the build process compared to the old system. (including custom esbuild plugin support)
- **Improved configuration**: We've migrated to using [c12](https://github.com/unjs/c12) to power our configuration system.
- **Improved error handling**: We now do a much better job of reporting of any errors that happen during the indexing process by loading your trigger task files dynamically.
- **Improved cold start times**: Previously, we would load all your trigger task files at once, which could lead to long cold start times. Now we load your trigger task files dynamically, which should improve cold start times.
## Update packages
To use the new build system, you have to update to use our preview packages. Update the `@trigger.dev/sdk` package in your package.json:
```json
"@trigger.dev/sdk": "0.0.0-prerelease-20240911144933"
```
You will also need to update your usage of the `trigger.dev` CLI to use the preview release. If you run the CLI via `npx` you can update to the preview release like so:
```sh
# old way
npx trigger.dev@latest dev
# using the preview release
npx trigger.dev@0.0.0-prerelease-20240911144933 dev
```
If you've added the `trigger.dev` CLI to your `devDependencies`, then you should update the version to point to the preview release:
```json
"trigger.dev": "0.0.0-prerelease-20240911144933"
```
Once you do that make sure you re-install your dependencies using `npm i` or the equivalent with your preferred package manager.
<Note>If you deploy using GitHub actions, make sure you update the version there too.</Note>
## Update your `trigger.config.ts`
The new build system does not effect your trigger task files at all, so those can remain unchanged. However, you may need to make changes to your `trigger.config.ts` file.
### `defineConfig`
You should now import the `defineConfig` function from `@trigger.dev/sdk` and export the config as the default export:
```
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
project: "<project ref>",
});
```
### Deprecated: `dependenciesToBundle`
The new build system will bundle all dependencies by default, so `dependenciesToBundle` no longer makes any sense and can be removed.
#### Externals
Now that all dependencies are bundled, there are some situations where bundling a dependency doesn't work, and needs to be made external (e.g. when a dependency includes a native module). You can now specify these dependencies as build externals in the `defineConfig` function:
```ts
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
project: "<project ref>",
build: {
external: ["native-module"],
},
});
```
`external` is an array of strings, where each string is the name of a dependency that should be made external. Glob expressions are also supported and use the [minimatch](https://github.com/isaacs/minimatch) matcher.
### `additionalFiles`
The `additionalFiles` option has been moved to our new build extension system.
To use build extensions, you'll need to add the `@trigger.dev/build` package to your `devDependencies`:
```sh
npm add @trigger.dev/build@0.0.0-prerelease-20240911144933 -D
```
Now you can import the `additionalFiles` build extension and use it in your `trigger.config.ts` file:
```ts
import { defineConfig } from "@trigger.dev/sdk";
import { additionalFiles } from "@trigger.dev/build/extensions/core";
export default defineConfig({
project: "<project ref>",
build: {
extensions: [
additionalFiles({ files: ["wrangler/wrangler.toml", "./assets/**", "./fonts/**"] }),
],
},
});
```
### `additionalPackages`
The `additionalPackages` option has been moved to our new build extension system.
To use build extensions, you'll need to add the `@trigger.dev/build` package to your `devDependencies`:
```sh
npm add @trigger.dev/build@0.0.0-prerelease-20240911144933 -D
```
Now you can import the `additionalPackages` build extension and use it in your `trigger.config.ts` file:
```ts
import { defineConfig } from "@trigger.dev/sdk";
import { additionalPackages } from "@trigger.dev/build/extensions/core";
export default defineConfig({
project: "<project ref>",
build: {
extensions: [additionalPackages({ packages: ["wrangler"] })],
},
});
```
### `resolveEnvVars`
The `resolveEnvVars` export has been moved to our new build extension system.
To use build extensions, you'll need to add the `@trigger.dev/build` package to your `devDependencies`:
```sh
npm add @trigger.dev/build@0.0.0-prerelease-20240911144933 -D
```
Now you can import the `syncEnvVars` build extension and use it in your `trigger.config.ts` file:
```ts
import { defineConfig } from "@trigger.dev/sdk";
import { syncEnvVars } from "@trigger.dev/build/extensions/core";
export default defineConfig({
project: "<project ref>",
build: {
extensions: [
syncEnvVars(async (params) => {
return {
MY_ENV_VAR: "my-value",
};
}),
],
},
});
```
The `syncEnvVars` callback function works very similarly to the [`resolveEnvVars`](/deploy-environment-variables#sync-env-vars-from-another-service) callback, but now instead of returning an object with a `variables` key that contains the environment variables, you return an object with the environment variables directly (see the example above).
One other difference is now `params.env` only contains the environment variables that are set in the Trigger.dev environment variables, and not the environment variables from the process. If you want to access the environment variables from the process, you can use `process.env`.
### emitDecoratorMetadata
If you make use of decorators in your code, and have enabled the `emitDecoratorMetadata` tsconfig compiler option, you'll need to enable this in the new build sytem using the `emitDecoratorMetadata` build extension:
```ts
import { defineConfig } from "@trigger.dev/sdk";
import { emitDecoratorMetadata } from "@trigger.dev/build/extensions/typescript";
export default defineConfig({
project: "<project ref>",
build: {
extensions: [emitDecoratorMetadata()],
},
});
```
### Prisma
We've created a build extension to support using Prisma in your Trigger.dev tasks. To use this extension, you'll need to add the `@trigger.dev/build` package to your `devDependencies`:
```sh
npm add @trigger.dev/build@0.0.0-prerelease-20240911144933 -D
```
Then you can import the `prismaExtension` build extension and use it in your `trigger.config.ts` file, passing in the path to your Prisma schema file:
```ts
import { defineConfig } from "@trigger.dev/sdk";
import { prismaExtension } from "@trigger.dev/build/extensions/prisma";
export default defineConfig({
project: "<project ref>",
build: {
extensions: [
prismaExtension({
schema: "prisma/schema.prisma",
}),
],
},
});
```
This will make sure that your prisma client is generated during the build process when deploying to Trigger.dev.
<Note>
This does not have any effect when running the `dev` command, so you'll need to make sure you
generate your client locally first.
</Note>
If you want to also run migrations during the build process, you can pass in the `migrate` option:
```ts
import { defineConfig } from "@trigger.dev/sdk";
import { prismaExtension } from "@trigger.dev/build/extensions/prisma";
export default defineConfig({
project: "<project ref>",
build: {
extensions: [
prismaExtension({
schema: "prisma/schema.prisma",
migrate: true,
directUrlEnvVarName: "DATABASE_URL_UNPOOLED", // optional - the name of the environment variable that contains the direct database URL if you are using a direct database URL
}),
],
},
});
```
If you have multiple `generator` statements defined in your schema file, you can pass in the `clientGenerator` option to specify the `prisma-client-js` generator, which will prevent other generators from being generated:
<CodeGroup>
```prisma schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DATABASE_URL_UNPOOLED")
}
// We only want to generate the prisma-client-js generator
generator client {
provider = "prisma-client-js"
}
generator kysely {
provider = "prisma-kysely"
output = "../../src/kysely"
enumFileName = "enums.ts"
fileName = "types.ts"
}
```
```ts trigger.config.ts
import { defineConfig } from "@trigger.dev/sdk";
import { prismaExtension } from "@trigger.dev/build/extensions/prisma";
export default defineConfig({
project: "<project ref>",
build: {
extensions: [
prismaExtension({
schema: "prisma/schema.prisma",
clientGenerator: "client",
}),
],
},
});
```
</CodeGroup>
### audioWaveform
Previously, we installed [Audio Waveform](https://github.com/bbc/audiowaveform) in the build image. That's been moved to a build extension:
```ts
import { defineConfig } from "@trigger.dev/sdk";
import { audioWaveform } from "@trigger.dev/build/extensions/audioWaveform";
export default defineConfig({
project: "<project ref>",
build: {
extensions: [audioWaveform()], // uses verson 1.1.0 of audiowaveform by default
},
});
```
### esbuild plugins
You can now add esbuild plugins to customize the build process using the `esbuildPlugin` build extension. The example below shows how to automatically upload sourcemaps to Sentry using their esbuild plugin:
```ts
import { defineConfig } from "@trigger.dev/sdk";
import { esbuildPlugin } from "@trigger.dev/build/extensions";
import { sentryEsbuildPlugin } from "@sentry/esbuild-plugin";
export default defineConfig({
project: "<project ref>",
build: {
extensions: [
esbuildPlugin(
sentryEsbuildPlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
}),
// optional - only runs during the deploy command, and adds the plugin to the end of the list of plugins
{ placement: "last", target: "deploy" }
),
],
},
});
```
## Changes to the `trigger.dev` CLI
### No more typechecking during deploy
We no longer run typechecking during the deploy command. This was causing issues with some projects, and we found that it wasn't necessary to run typechecking during the deploy command. If you want to run typechecking before deploying to Trigger.dev, you can run the `tsc` command before running the `deploy` command.
```sh
tsc && npx trigger.dev@0.0.0-prerelease-20240911144933 deploy
```
Or if you are using GitHub actions, you can add an additional step to run the `tsc` command before deploying to Trigger.dev.
```yaml
- name: Install dependencies
run: npm install
- name: Typecheck
run: npx tsc
- name: 🚀 Deploy Trigger.dev
env:
TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}
run: |
npx trigger.dev@0.0.0-prerelease-20240911144933 deploy
```
### deploy --dry-run
You can now inspect the build output of your project without actually deploying it to Trigger.dev by using the `--dry-run` flag:
```sh
npx trigger.dev@0.0.0-prerelease-20240911144933 deploy --dry-run
```
This will save the build output and print the path to the build output directory. If you face any issues with deploying, please include the build output in your issue report.
### --env-file
You can now pass the path to your local `.env` file using the `--env-file` flag during `dev` and `deploy` commands:
```sh
npx trigger.dev@0.0.0-prerelease-20240911144933 dev --env-file ../../.env
npx trigger.dev@0.0.0-prerelease-20240911144933 deploy --env-file ../../.env
```
The `.env` file works slightly differently in `dev` vs `deploy`:
- In `dev`, the `.env` file is loaded into the CLI's `process.env` and also into the environment variables of the Trigger.dev environment.
- In `deploy`, the `.env` file is loaded into the CLI's `process.env` but not into the environment variables of the Trigger.dev environment. If you want to sync the environment variables from the `.env` file to the Trigger.dev environment variables, you can use the `syncEnvVars` build extension.
### dev debugging in VS Code
Debugging your tasks code in `dev` is now supported via VS Code, without having to pass in any additional flags. Create a launch configuration in `.vscode/launch.json`:
```json launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Trigger.dev: Dev",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "npx",
"runtimeArgs": ["trigger.dev@0.0.0-prerelease-20240911144933", "dev"],
"skipFiles": ["<node_internals>/**"],
"sourceMaps": true
}
]
}
```
Then you can start debugging your tasks code by selecting the `Trigger.dev: Dev` configuration in the debug panel, and set breakpoints in your tasks code.
### TRIGGER_ACCESS_TOKEN in dev
You can now authenticate the `dev` command using the `TRIGGER_ACCESS_TOKEN` environment variable. Previously this was only supported in the `deploy` command.
```sh
TRIGGER_ACCESS_TOKEN=<your access token> npx trigger.dev@0.0.0-prerelease-20240911144933 dev
```
### Better deploy support for self-hosters
You can now specify a custom registry and namespace when deploying via a self-hosted instance of Trigger.dev:
```sh
npx trigger.dev@0.0.0-prerelease-20240911144933 deploy --self-hosted --load-image --push --registry docker.io --namespace mydockerhubusername
```
All you have to do is create a repository in dockerhub that matches the project ref of your Trigger.dev project (e.g. `proj_rrkpdguyagvsoktglnod`)
<Note>
Docker Hub will automatically create a repository the first time you push, which is public by
default. If you want to keep these images private, make sure you create the repository before you
first run the `deploy` command
</Note>
## Known issues
- Path aliases are not yet support in your `trigger.config.ts` file. To workaround this issue you'll need to rewrite path aliases to their relative paths. (See [this](https://github.com/unjs/jiti/issues/166) and [this](https://knip.dev/reference/known-issues#path-aliases-in-config-files)) for more info.
- `*.test.ts` and `.spec.ts` files inside the trigger dirs will be bundled and could cause issues. You'll need to move these files outside of the trigger dirs to avoid this issue.
## Changelog
### Changes and fixes in `0.0.0-prerelease-20240911144933`
- Fixed an issue where empty env vars in dev runs were overriding local `.env` file values.
- Fixed an issue when importing v2 `@trigger.dev/sdk` would throw an error because of a missing package.json
- Fixed node10 moduleResolution with some types
- Added the ability to push to a custom registry when deploying to a self-hosted instance of Trigger.dev
- Fix stuck dev runs when a child run fails with a process.exit
- No longer ignoring `--project-ref` in the `deploy` command
- No longer ignoring the `--config` option in the `deploy` command
- Fixed the flushing of logs to the server when deployed.
- `emitDecoratorMetadata` build extension now works when tsconfig.json file extends another tsconfig.json file
- We now have the ability to force certain packages to be external (i.e. not be bundled), starting with `headers-generator`
- You can now call `init` with the `--javascript` flag to initialize a project with JavaScript instead of TypeScript
- Add support for Prisma TypedSQL in the prisma extension
+228
View File
@@ -0,0 +1,228 @@
---
title: "Python headless browser web crawler example"
sidebarTitle: "Headless web crawler"
description: "Learn how to use Python, Crawl4AI and Playwright to create a headless browser web crawler with Trigger.dev."
---
import ScrapingWarning from "/snippets/web-scraping-warning.mdx";
import PythonLearnMore from "/snippets/python-learn-more.mdx";
## Overview
This demo showcases how to use Trigger.dev with Python to build a web crawler that uses a headless browser to navigate websites and extract content.
## Prerequisites
- A project with [Trigger.dev initialized](/quick-start)
- [Python](https://www.python.org/) installed on your local machine
## Features
- [Trigger.dev](https://trigger.dev) for background task orchestration
- Our [Python build extension](/config/extensions/pythonExtension) to install the dependencies and run the Python script
- [Crawl4AI](https://github.com/unclecode/crawl4ai), an open source LLM friendly web crawler
- A custom [Playwright extension](https://playwright.dev/) to create a headless chromium browser
- Proxy support
## Using Proxies
<ScrapingWarning />
Some popular proxy services are:
- [Smartproxy](https://smartproxy.com/)
- [Bright Data](https://brightdata.com/)
- [Browserbase](https://browserbase.com/)
- [Oxylabs](https://oxylabs.io/)
- [ScrapingBee](https://scrapingbee.com/)
Once you have a proxy service, set the following environment variables in your Trigger.dev .env file, and add them in the Trigger.dev dashboard:
- `PROXY_URL`: The URL of your proxy server (e.g., `http://proxy.example.com:8080`)
- `PROXY_USERNAME`: Username for authenticated proxies (optional)
- `PROXY_PASSWORD`: Password for authenticated proxies (optional)
## GitHub repo
<Card
title="View the project on GitHub"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/python-crawl4ai"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## The code
### Build configuration
After you've initialized your project with Trigger.dev, add these build settings to your `trigger.config.ts` file:
```ts trigger.config.ts
import { defineConfig } from "@trigger.dev/sdk";
import { pythonExtension } from "@trigger.dev/python/extension";
import type { BuildContext, BuildExtension } from "@trigger.dev/core/build";
export default defineConfig({
project: "<project ref>",
// Your other config settings...
build: {
extensions: [
// This is required to use the Python extension
pythonExtension(),
// This is required to create a headless chromium browser with Playwright
installPlaywrightChromium(),
],
},
});
// This is a custom build extension to install Playwright and Chromium
export function installPlaywrightChromium(): BuildExtension {
return {
name: "InstallPlaywrightChromium",
onBuildComplete(context: BuildContext) {
const instructions = [
// Base and Chromium dependencies
`RUN apt-get update && apt-get install -y --no-install-recommends \
curl unzip npm libnspr4 libatk1.0-0 libatk-bridge2.0-0 libatspi2.0-0 \
libasound2 libnss3 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
libgbm1 libxkbcommon0 \
&& apt-get clean && rm -rf /var/lib/apt/lists/*`,
// Install Playwright and Chromium
`RUN npm install -g playwright`,
`RUN mkdir -p /ms-playwright`,
`RUN PLAYWRIGHT_BROWSERS_PATH=/ms-playwright python -m playwright install --with-deps chromium`,
];
context.addLayer({
id: "playwright",
image: { instructions },
deploy: {
env: {
PLAYWRIGHT_BROWSERS_PATH: "/ms-playwright",
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1",
PLAYWRIGHT_SKIP_BROWSER_VALIDATION: "1",
},
override: true,
},
});
},
};
}
```
<Info>
Learn more about executing scripts in your Trigger.dev project using our Python build extension
[here](/config/extensions/pythonExtension).
</Info>
### Task code
This task uses the `python.runScript` method to run the `crawl-url.py` script with the given URL as an argument. You can see the original task in our examples repository [here](https://github.com/triggerdotdev/examples/blob/main/python-crawl4ai/src/trigger/pythonTasks.ts).
```ts src/trigger/pythonTasks.ts
import { logger, schemaTask, task } from "@trigger.dev/sdk";
import { python } from "@trigger.dev/python";
import { z } from "zod";
export const convertUrlToMarkdown = schemaTask({
id: "convert-url-to-markdown",
schema: z.object({
url: z.string().url(),
}),
run: async (payload) => {
// Pass through any proxy environment variables
const env = {
PROXY_URL: process.env.PROXY_URL,
PROXY_USERNAME: process.env.PROXY_USERNAME,
PROXY_PASSWORD: process.env.PROXY_PASSWORD,
};
const result = await python.runScript("./src/python/crawl-url.py", [payload.url], { env });
logger.debug("convert-url-to-markdown", {
url: payload.url,
result,
});
return result.stdout;
},
});
```
### Add a requirements.txt file
Add the following to your `requirements.txt` file. This is required in Python projects to install the dependencies.
```txt requirements.txt
crawl4ai
playwright
urllib3<2.0.0
```
### The Python script
The Python script is a simple script using Crawl4AI that takes a URL and returns the markdown content of the page. You can see the original script in our examples repository [here](https://github.com/triggerdotdev/examples/blob/main/python-crawl4ai/src/python/crawl-url.py).
```python src/python/crawl-url.py
import asyncio
import sys
import os
from crawl4ai import *
from crawl4ai.async_configs import BrowserConfig
async def main(url: str):
# Get proxy configuration from environment variables
proxy_url = os.environ.get("PROXY_URL")
proxy_username = os.environ.get("PROXY_USERNAME")
proxy_password = os.environ.get("PROXY_PASSWORD")
# Configure the proxy
browser_config = None
if proxy_url:
if proxy_username and proxy_password:
# Use authenticated proxy
proxy_config = {
"server": proxy_url,
"username": proxy_username,
"password": proxy_password
}
browser_config = BrowserConfig(proxy_config=proxy_config)
else:
# Use simple proxy
browser_config = BrowserConfig(proxy=proxy_url)
else:
browser_config = BrowserConfig()
async with AsyncWebCrawler(config=browser_config) as crawler:
result = await crawler.arun(
url=url,
)
print(result.markdown)
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python crawl-url.py <url>")
sys.exit(1)
url = sys.argv[1]
asyncio.run(main(url))
```
## Testing your task
1. Create a virtual environment `python -m venv venv`
2. Activate the virtual environment, depending on your OS: On Mac/Linux: `source venv/bin/activate`, on Windows: `venv\Scripts\activate`
3. Install the Python dependencies `pip install -r requirements.txt`
4. If you haven't already, copy your project ref from your [Trigger.dev dashboard](https://cloud.trigger.dev) and add it to the `trigger.config.ts` file.
5. Run the Trigger.dev CLI `dev` command (it may ask you to authorize the CLI if you haven't already).
6. Test the task in the dashboard, using a URL of your choice.
<ScrapingWarning />
## Deploying your task
Deploy the task to production using the Trigger.dev CLI `deploy` command.
<PythonLearnMore />
@@ -0,0 +1,219 @@
---
title: "Convert documents to markdown using Python and MarkItDown"
sidebarTitle: "Convert docs to markdown"
description: "Learn how to use Trigger.dev with Python to convert documents to markdown using MarkItDown."
---
import PythonLearnMore from "/snippets/python-learn-more.mdx";
## Overview
Convert documents to markdown using Microsoft's [MarkItDown](https://github.com/microsoft/markitdown) library. This can be especially useful for preparing documents in a structured format for AI applications.
## Prerequisites
- A project with [Trigger.dev initialized](/quick-start)
- [Python](https://www.python.org/) installed on your local machine. _This example requires Python 3.10 or higher._
## Features
- A Trigger.dev task which downloads a document from a URL and runs the Python script which converts it to markdown
- A Python script to convert documents to markdown using Microsoft's [MarkItDown](https://github.com/microsoft/markitdown) library
- Uses our [Python build extension](/config/extensions/pythonExtension) to install dependencies and run Python scripts
## GitHub repo
<Card
title="View the project on GitHub"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/python-doc-to-markdown-converter"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## The code
### Build configuration
After you've initialized your project with Trigger.dev, add these build settings to your `trigger.config.ts` file:
```ts trigger.config.ts
import { pythonExtension } from "@trigger.dev/python/extension";
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
runtime: "node",
project: "<your-project-ref>",
// Your other config settings...
build: {
extensions: [
pythonExtension({
// The path to your requirements.txt file
requirementsFile: "./requirements.txt",
// The path to your Python binary
devPythonBinaryPath: `venv/bin/python`,
// The paths to your Python scripts to run
scripts: ["src/python/**/*.py"],
}),
],
},
});
```
<Info>
Learn more about executing scripts in your Trigger.dev project using our Python build extension
[here](/config/extensions/pythonExtension).
</Info>
### Task code
This task uses the `python.runScript` method to run the `markdown-converter.py` script with the given document URL as an argument.
```ts src/trigger/convertToMarkdown.ts
import { task } from "@trigger.dev/sdk";
import { python } from "@trigger.dev/python";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
export const convertToMarkdown = task({
id: "convert-to-markdown",
run: async (payload: { url: string }) => {
const { url } = payload;
// STEP 1: Create temporary file with unique name
const tempDir = os.tmpdir();
const fileName = `doc-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`;
const urlPath = new URL(url).pathname;
const extension = path.extname(urlPath) || ".docx";
const tempFilePath = path.join(tempDir, `${fileName}${extension}`);
// STEP 2: Download file from URL
const response = await fetch(url);
const buffer = await response.arrayBuffer();
await fs.promises.writeFile(tempFilePath, Buffer.from(buffer));
// STEP 3: Run Python script to convert document to markdown
const pythonResult = await python.runScript("./src/python/markdown-converter.py", [
JSON.stringify({ file_path: tempFilePath }),
]);
// STEP 4: Clean up temporary file
fs.unlink(tempFilePath, () => {});
// STEP 5: Process result
if (pythonResult.stdout) {
const result = JSON.parse(pythonResult.stdout);
return {
url,
markdown: result.status === "success" ? result.markdown : null,
error: result.status === "error" ? result.error : null,
success: result.status === "success",
};
}
return {
url,
markdown: null,
error: "No output from Python script",
success: false,
};
},
});
```
### Add a requirements.txt file
Add the following to your `requirements.txt` file. This is required in Python projects to install the dependencies.
```txt requirements.txt
markitdown[all]
```
### The Python script
The Python script uses MarkItDown to convert documents to Markdown format.
```python src/python/markdown-converter.py
import json
import sys
import os
from markitdown import MarkItDown
def convert_to_markdown(file_path):
"""Convert a file to markdown format using MarkItDown"""
# Check if file exists
if not os.path.exists(file_path):
raise FileNotFoundError(f"File not found: {file_path}")
# Initialize MarkItDown
md = MarkItDown()
# Convert the file
try:
result = md.convert(file_path)
return result.text_content
except Exception as e:
raise Exception(f"Error converting file: {str(e)}")
def process_trigger_task(file_path):
"""Process a file and convert to markdown"""
try:
markdown_result = convert_to_markdown(file_path)
return {
"status": "success",
"markdown": markdown_result
}
except Exception as e:
return {
"status": "error",
"error": str(e)
}
if __name__ == "__main__":
# Get the file path from command line arguments
if len(sys.argv) < 2:
print(json.dumps({"status": "error", "error": "No file path provided"}))
sys.exit(1)
try:
config = json.loads(sys.argv[1])
file_path = config.get("file_path")
if not file_path:
print(json.dumps({"status": "error", "error": "No file path specified in config"}))
sys.exit(1)
result = process_trigger_task(file_path)
print(json.dumps(result))
except Exception as e:
print(json.dumps({"status": "error", "error": str(e)}))
sys.exit(1)
```
## Testing your task
1. Create a virtual environment `python -m venv venv`
2. Activate the virtual environment, depending on your OS: On Mac/Linux: `source venv/bin/activate`, on Windows: `venv\Scripts\activate`
3. Install the Python dependencies `pip install -r requirements.txt`. _Make sure you have Python 3.10 or higher installed._
4. Copy the project ref from your [Trigger.dev dashboard](https://cloud.trigger.dev) and add it to the `trigger.config.ts` file.
5. Run the Trigger.dev CLI `dev` command (it may ask you to authorize the CLI if you haven't already).
6. Test the task in the dashboard by providing a valid document URL.
7. Deploy the task to production using the Trigger.dev CLI `deploy` command.
## MarkItDown Conversion Capabilities
- Convert various file formats to Markdown:
- Office formats (Word, PowerPoint, Excel)
- PDFs
- Images (with optional LLM-generated descriptions)
- HTML, CSV, JSON, XML
- Audio files (with optional transcription)
- ZIP archives
- And more
- Preserve document structure (headings, lists, tables, etc.)
- Handle multiple input methods (file paths, URLs, base64 data)
- Optional Azure Document Intelligence integration for better PDF and image conversion
<PythonLearnMore />
@@ -0,0 +1,547 @@
---
title: "Python image processing example"
sidebarTitle: "Process images"
description: "Learn how to use Trigger.dev with Python to process images from URLs and upload them to S3."
---
import PythonLearnMore from "/snippets/python-learn-more.mdx";
## Overview
This demo showcases how to use Trigger.dev with Python to process an image using Pillow (PIL) from a URL and upload it to S3-compatible storage bucket.
## Prerequisites
- A project with [Trigger.dev initialized](/quick-start)
- [Python](https://www.python.org/) installed on your local machine
## Features
- A [Trigger.dev](https://trigger.dev) task to trigger the image processing Python script, and then upload the processed image to S3-compatible storage
- The [Trigger.dev Python build extension](https://trigger.dev/docs/config/extensions/pythonExtension) to install dependencies and run Python scripts
- [Pillow (PIL)](https://pillow.readthedocs.io/) for powerful image processing capabilities
- [AWS SDK v3](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/) for S3 uploads
- S3-compatible storage support (AWS S3, Cloudflare R2, etc.)
## GitHub repo
<Card
title="View the project on GitHub"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/tree/main/python-image-processing"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## The code
### Build configuration
After you've initialized your project with Trigger.dev, add these build settings to your `trigger.config.ts` file:
```ts trigger.config.ts
import { pythonExtension } from "@trigger.dev/python/extension";
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
runtime: "node",
project: "<your-project-ref>",
// Your other config settings...
build: {
extensions: [
pythonExtension({
// The path to your requirements.txt file
requirementsFile: "./requirements.txt",
// The path to your Python binary
devPythonBinaryPath: `venv/bin/python`,
// The paths to your Python scripts to run
scripts: ["src/python/**/*.py"],
}),
],
},
});
```
<Info>
Learn more about executing scripts in your Trigger.dev project using our Python build extension
[here](/config/extensions/pythonExtension).
</Info>
### Task code
This task uses the `python.runScript` method to run the `image-processing.py` script with the given image URL as an argument. You can adjust the image processing parameters in the payload, with options such as height, width, quality, output format, etc.
```ts src/trigger/processImage.ts
import { schemaTask } from "@trigger.dev/sdk";
import { z } from "zod";
import { python } from "@trigger.dev/python";
import { promises as fs } from "fs";
import { S3Client } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
// Initialize S3 client
const s3Client = new S3Client({
region: "auto",
endpoint: process.env.S3_ENDPOINT,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY ?? "",
},
});
// Define the input schema with Zod
const imageProcessingSchema = z.object({
imageUrl: z.string().url(),
height: z.number().positive().optional().default(800),
width: z.number().positive().optional().default(600),
quality: z.number().min(1).max(100).optional().default(85),
maintainAspectRatio: z.boolean().optional().default(true),
outputFormat: z.enum(["jpeg", "png", "webp", "gif", "avif"]).optional().default("jpeg"),
brightness: z.number().optional(),
contrast: z.number().optional(),
sharpness: z.number().optional(),
grayscale: z.boolean().optional().default(false),
});
// Define the output schema
const outputSchema = z.object({
url: z.string().url(),
key: z.string(),
format: z.string(),
originalSize: z.object({
width: z.number(),
height: z.number(),
}),
newSize: z.object({
width: z.number(),
height: z.number(),
}),
fileSizeBytes: z.number(),
exitCode: z.number(),
});
export const processImage = schemaTask({
id: "process-image",
schema: imageProcessingSchema,
run: async (payload, io) => {
const {
imageUrl,
height,
width,
quality,
maintainAspectRatio,
outputFormat,
brightness,
contrast,
sharpness,
grayscale,
} = payload;
try {
// Run the Python script
const result = await python.runScript("./src/python/image-processing.py", [
imageUrl,
height.toString(),
width.toString(),
quality.toString(),
maintainAspectRatio.toString(),
outputFormat,
brightness?.toString() || "null",
contrast?.toString() || "null",
sharpness?.toString() || "null",
grayscale.toString(),
]);
const { outputPath, format, originalSize, newSize, fileSizeBytes } = JSON.parse(
result.stdout
);
// Read file once
const fileContent = await fs.readFile(outputPath);
try {
// Upload to S3
const key = `processed-images/${Date.now()}-${outputPath.split("/").pop()}`;
await new Upload({
client: s3Client,
params: {
Bucket: process.env.S3_BUCKET!,
Key: key,
Body: fileContent,
ContentType: `image/${format}`,
},
}).done();
return {
url: `${process.env.S3_PUBLIC_URL}/${key}`,
key,
format,
originalSize,
newSize,
fileSizeBytes,
exitCode: result.exitCode,
};
} finally {
// Always clean up the temp file
await fs.unlink(outputPath).catch(console.error);
}
} catch (error) {
throw new Error(
`Processing failed: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
},
});
```
### Add a requirements.txt file
Add the following to your `requirements.txt` file. This is required in Python projects to install the dependencies.
```txt requirements.txt
# Core dependencies
Pillow==10.2.0 # Image processing library
python-dotenv==1.0.0 # Environment variable management
requests==2.31.0 # HTTP requests
numpy==1.26.3 # Numerical operations (for advanced processing)
# Optional enhancements
opencv-python==4.8.1.78 # For more advanced image processing
```
### The Python script
The Python script uses Pillow (PIL) to process an image. You can see the original script in our examples repository [here](https://github.com/triggerdotdev/examples/blob/main/python-image-processing/src/python/image-processing.py).
```python src/python/image-processing.py
from PIL import Image, ImageOps, ImageEnhance
import io
from io import BytesIO
import os
from typing import Tuple, List, Dict, Optional, Union
import logging
import sys
import json
import requests
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class ImageProcessor:
"""Image processing utility for resizing, optimizing, and converting images."""
# Supported formats for conversion
SUPPORTED_FORMATS = ['JPEG', 'PNG', 'WEBP', 'GIF', 'AVIF']
@staticmethod
def open_image(image_data: Union[bytes, str]) -> Image.Image:
"""Open an image from bytes or file path."""
try:
if isinstance(image_data, bytes):
return Image.open(io.BytesIO(image_data))
else:
return Image.open(image_data)
except Exception as e:
logger.error(f"Failed to open image: {e}")
raise ValueError(f"Could not open image: {e}")
@staticmethod
def resize_image(
img: Image.Image,
width: Optional[int] = None,
height: Optional[int] = None,
maintain_aspect_ratio: bool = True
) -> Image.Image:
"""
Resize an image to specified dimensions.
Args:
img: PIL Image object
width: Target width (None to auto-calculate from height)
height: Target height (None to auto-calculate from width)
maintain_aspect_ratio: Whether to maintain the original aspect ratio
Returns:
Resized PIL Image
"""
if width is None and height is None:
return img # No resize needed
original_width, original_height = img.size
if maintain_aspect_ratio:
if width and height:
# Calculate the best fit while maintaining aspect ratio
ratio = min(width / original_width, height / original_height)
new_width = int(original_width * ratio)
new_height = int(original_height * ratio)
elif width:
# Calculate height based on width
ratio = width / original_width
new_width = width
new_height = int(original_height * ratio)
else:
# Calculate width based on height
ratio = height / original_height
new_width = int(original_width * ratio)
new_height = height
else:
# Force exact dimensions
new_width = width if width else original_width
new_height = height if height else original_height
return img.resize((new_width, new_height), Image.LANCZOS)
@staticmethod
def optimize_image(
img: Image.Image,
quality: int = 85,
format: Optional[str] = None
) -> Tuple[bytes, str]:
"""
Optimize an image for web delivery.
Args:
img: PIL Image object
quality: JPEG/WebP quality (0-100)
format: Output format (JPEG, PNG, WEBP, etc.)
Returns:
Tuple of (image_bytes, format)
"""
if format is None:
format = img.format or 'JPEG'
format = format.upper()
if format not in ImageProcessor.SUPPORTED_FORMATS:
format = 'JPEG' # Default to JPEG if unsupported format
# Convert mode if needed
if format == 'JPEG' and img.mode in ('RGBA', 'P'):
img = img.convert('RGB')
# Save to bytes
buffer = io.BytesIO()
if format == 'JPEG':
img.save(buffer, format=format, quality=quality, optimize=True)
elif format == 'PNG':
img.save(buffer, format=format, optimize=True)
elif format == 'WEBP':
img.save(buffer, format=format, quality=quality)
elif format == 'AVIF':
img.save(buffer, format=format, quality=quality)
else:
img.save(buffer, format=format)
buffer.seek(0)
return buffer.getvalue(), format.lower()
@staticmethod
def apply_filters(
img: Image.Image,
brightness: Optional[float] = None,
contrast: Optional[float] = None,
sharpness: Optional[float] = None,
grayscale: bool = False
) -> Image.Image:
"""
Apply various filters and enhancements to an image.
Args:
img: PIL Image object
brightness: Brightness factor (0.0-2.0, 1.0 is original)
contrast: Contrast factor (0.0-2.0, 1.0 is original)
sharpness: Sharpness factor (0.0-2.0, 1.0 is original)
grayscale: Convert to grayscale if True
Returns:
Processed PIL Image
"""
# Apply grayscale first if requested
if grayscale:
img = ImageOps.grayscale(img)
# Convert back to RGB if other filters will be applied
if any(x is not None for x in [brightness, contrast, sharpness]):
img = img.convert('RGB')
# Apply enhancements
if brightness is not None:
img = ImageEnhance.Brightness(img).enhance(brightness)
if contrast is not None:
img = ImageEnhance.Contrast(img).enhance(contrast)
if sharpness is not None:
img = ImageEnhance.Sharpness(img).enhance(sharpness)
return img
@staticmethod
def process_image(
image_data: Union[bytes, str],
width: Optional[int] = None,
height: Optional[int] = None,
maintain_aspect_ratio: bool = True,
quality: int = 85,
output_format: Optional[str] = None,
brightness: Optional[float] = None,
contrast: Optional[float] = None,
sharpness: Optional[float] = None,
grayscale: bool = False
) -> Dict:
"""
Process an image with all available options.
Args:
image_data: Image bytes or file path
width: Target width
height: Target height
maintain_aspect_ratio: Whether to maintain aspect ratio
quality: Output quality
output_format: Output format
brightness: Brightness adjustment
contrast: Contrast adjustment
sharpness: Sharpness adjustment
grayscale: Convert to grayscale
Returns:
Dict with processed image data and metadata
"""
# Open the image
img = ImageProcessor.open_image(image_data)
original_format = img.format
original_size = img.size
# Apply filters
img = ImageProcessor.apply_filters(
img,
brightness=brightness,
contrast=contrast,
sharpness=sharpness,
grayscale=grayscale
)
# Resize if needed
if width or height:
img = ImageProcessor.resize_image(
img,
width=width,
height=height,
maintain_aspect_ratio=maintain_aspect_ratio
)
# Optimize and get bytes
processed_bytes, actual_format = ImageProcessor.optimize_image(
img,
quality=quality,
format=output_format
)
# Return result with metadata
return {
"processed_image": processed_bytes,
"format": actual_format,
"original_format": original_format,
"original_size": original_size,
"new_size": img.size,
"file_size_bytes": len(processed_bytes)
}
def process_image(url, height, width, quality):
# Download image from URL
response = requests.get(url)
img = Image.open(BytesIO(response.content))
# Resize
img = img.resize((int(width), int(height)), Image.Resampling.LANCZOS)
# Save with quality setting
output_path = f"/tmp/processed_{width}x{height}.jpg"
img.save(output_path, "JPEG", quality=int(quality))
return output_path
if __name__ == "__main__":
url = sys.argv[1]
height = int(sys.argv[2])
width = int(sys.argv[3])
quality = int(sys.argv[4])
maintain_aspect_ratio = sys.argv[5].lower() == 'true'
output_format = sys.argv[6]
brightness = float(sys.argv[7]) if sys.argv[7] != 'null' else None
contrast = float(sys.argv[8]) if sys.argv[8] != 'null' else None
sharpness = float(sys.argv[9]) if sys.argv[9] != 'null' else None
grayscale = sys.argv[10].lower() == 'true'
processor = ImageProcessor()
result = processor.process_image(
requests.get(url).content,
width=width,
height=height,
maintain_aspect_ratio=maintain_aspect_ratio,
quality=quality,
output_format=output_format,
brightness=brightness,
contrast=contrast,
sharpness=sharpness,
grayscale=grayscale
)
output_path = f"/tmp/processed_{width}x{height}.{result['format']}"
with open(output_path, 'wb') as f:
f.write(result['processed_image'])
print(json.dumps({
"outputPath": output_path,
"format": result['format'],
"originalSize": result['original_size'],
"newSize": result['new_size'],
"fileSizeBytes": result['file_size_bytes']
}))
```
## Testing your task
1. Create a virtual environment `python -m venv venv`
2. Activate the virtual environment, depending on your OS: On Mac/Linux: `source venv/bin/activate`, on Windows: `venv\Scripts\activate`
3. Install the Python dependencies `pip install -r requirements.txt`
4. Set up your S3-compatible storage credentials in your environment variables, in .env for local development, or in the Trigger.dev dashboard for production:
```
S3_ENDPOINT=https://your-endpoint.com
S3_ACCESS_KEY_ID=your-access-key
S3_SECRET_ACCESS_KEY=your-secret-key
S3_BUCKET=your-bucket-name
S3_PUBLIC_URL=https://your-public-url.com
```
5. Copy the project ref from your [Trigger.dev dashboard](https://cloud.trigger.dev) and add it to the `trigger.config.ts` file.
6. Run the Trigger.dev CLI `dev` command (it may ask you to authorize the CLI if you haven't already).
7. Test the task in the dashboard by providing a valid image URL and processing options.
8. Deploy the task to production using the Trigger.dev CLI `deploy` command.
## Example Payload
These are all optional parameters that can be passed to the `image-processing.py` Python script from the `processImage.ts` task.
```json
{
"imageUrl": "<your-image-url>",
"height": 1200,
"width": 900,
"quality": 90,
"maintainAspectRatio": true,
"outputFormat": "webp",
"brightness": 1.2,
"contrast": 1.1,
"sharpness": 1.3,
"grayscale": false
}
```
## Deploying your task
Deploy the task to production using the CLI command `npx trigger.dev@latest deploy`
<PythonLearnMore />
@@ -0,0 +1,194 @@
---
title: "Python PDF form extractor example"
sidebarTitle: "Extract form data from PDFs"
description: "Learn how to use Trigger.dev with Python to extract form data from PDF files."
---
import PythonLearnMore from "/snippets/python-learn-more.mdx";
## Overview
This demo showcases how to use Trigger.dev with Python to extract structured form data from a PDF file available at a URL.
## Prerequisites
- A project with [Trigger.dev initialized](/quick-start)
- [Python](https://www.python.org/) installed on your local machine
## Features
- A [Trigger.dev](https://trigger.dev) task to trigger the Python script
- [Trigger.dev Python build extension](https://trigger.dev/docs/config/extensions/pythonExtension) to install the dependencies and run the Python script
- [PyMuPDF](https://pymupdf.readthedocs.io/en/latest/) to extract form data from PDF files
- [Requests](https://docs.python-requests.org/en/master/) to download PDF files from URLs
## GitHub repo
<Card
title="View the project on GitHub"
icon="GitHub"
href="https://github.com/triggerdotdev/examples/edit/main/python-pdf-form-extractor/"
>
Click here to view the full code for this project in our examples repository on GitHub. You can
fork it and use it as a starting point for your own project.
</Card>
## The code
### Build configuration
After you've initialized your project with Trigger.dev, add these build settings to your `trigger.config.ts` file:
```ts trigger.config.ts
import { pythonExtension } from "@trigger.dev/python/extension";
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
runtime: "node",
project: "<your-project-ref>",
// Your other config settings...
build: {
extensions: [
pythonExtension({
// The path to your requirements.txt file
requirementsFile: "./requirements.txt",
// The path to your Python binary
devPythonBinaryPath: `venv/bin/python`,
// The paths to your Python scripts to run
scripts: ["src/python/**/*.py"],
}),
],
},
});
```
<Info>
Learn more about executing scripts in your Trigger.dev project using our Python build extension
[here](/config/extensions/pythonExtension).
</Info>
### Task code
This task uses the `python.runScript` method to run the `image-processing.py` script with the given image URL as an argument. You can adjust the image processing parameters in the payload, with options such as height, width, quality, output format, etc.
```ts src/trigger/pythonPdfTask.ts
import { task } from "@trigger.dev/sdk";
import { python } from "@trigger.dev/python";
export const processPdfForm = task({
id: "process-pdf-form",
run: async (payload: { pdfUrl: string }, io: any) => {
const { pdfUrl } = payload;
const args = [pdfUrl];
const result = await python.runScript("./src/python/extract-pdf-form.py", args);
// Parse the JSON output from the script
let formData;
try {
formData = JSON.parse(result.stdout);
} catch (error) {
throw new Error(`Failed to parse JSON output: ${result.stdout}`);
}
return {
formData,
stderr: result.stderr,
exitCode: result.exitCode,
};
},
});
```
### Add a requirements.txt file
Add the following to your `requirements.txt` file. This is required in Python projects to install the dependencies.
```txt requirements.txt
PyMuPDF==1.23.8
requests==2.31.0
```
### The Python script
The Python script uses PyMuPDF to extract form data from a PDF file. You can see the original script in our examples repository [here](https://github.com/triggerdotdev/examples/blob/main/python-pdf-form-extractor/src/python/extract-pdf-form.py).
```python src/python/extract-pdf-form.py
import fitz # PyMuPDF
import requests
import os
import json
import sys
from urllib.parse import urlparse
def download_pdf(url):
"""Download PDF from URL to a temporary file"""
response = requests.get(url)
response.raise_for_status()
# Get filename from URL or use default
filename = os.path.basename(urlparse(url).path) or "downloaded.pdf"
filepath = os.path.join("/tmp", filename)
with open(filepath, 'wb') as f:
f.write(response.content)
return filepath
def extract_form_data(pdf_path):
"""Extract form data from a PDF file."""
doc = fitz.open(pdf_path)
form_data = {}
for page_num, page in enumerate(doc):
fields = page.widgets()
for field in fields:
field_name = field.field_name or f"unnamed_field_{page_num}_{len(form_data)}"
field_type = field.field_type_string
field_value = field.field_value
# For checkboxes, convert to boolean
if field_type == "CheckBox":
field_value = field_value == "Yes"
form_data[field_name] = {
"type": field_type,
"value": field_value,
"page": page_num + 1
}
return form_data
def main():
if len(sys.argv) < 2:
print(json.dumps({"error": "PDF URL is required as an argument"}), file=sys.stderr)
return 1
url = sys.argv[1]
try:
pdf_path = download_pdf(url)
form_data = extract_form_data(pdf_path)
# Convert to JSON for structured output
structured_output = json.dumps(form_data, indent=2)
print(structured_output)
return 0
except Exception as e:
print(json.dumps({"error": str(e)}), file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())
```
## Testing your task
1. Create a virtual environment `python -m venv venv`
2. Activate the virtual environment, depending on your OS: On Mac/Linux: `source venv/bin/activate`, on Windows: `venv\Scripts\activate`
3. Install the Python dependencies `pip install -r requirements.txt`
4. Copy the project ref from your [Trigger.dev dashboard](https://cloud.trigger.dev) and add it to the `trigger.config.ts` file.
5. Run the Trigger.dev CLI `dev` command (it may ask you to authorize the CLI if you haven't already).
6. Test the task in the dashboard by providing a valid PDF URL.
7. Deploy the task to production using the Trigger.dev CLI `deploy` command.
<PythonLearnMore />
@@ -0,0 +1,159 @@
---
title: "Data processing & ETL workflows"
sidebarTitle: "Data processing & ETL"
description: "Learn how to use Trigger.dev for data processing and ETL (Extract, Transform, Load), including web scraping, database synchronization, batch enrichment and more."
---
import UseCasesCards from "/snippets/use-cases-cards.mdx";
## Overview
Build complex data pipelines that process large datasets without timeouts. Handle streaming analytics, batch enrichment, web scraping, database sync, and file processing with automatic retries and progress tracking.
## Featured examples
<CardGroup cols={3}>
<Card
title="Realtime CSV importer"
icon="book"
href="/guides/example-projects/realtime-csv-importer"
>
Import CSV files with progress streamed live to frontend.
</Card>
<Card title="Web scraper with BrowserBase" icon="book" href="/guides/examples/scrape-hacker-news">
Scrape websites using BrowserBase and Puppeteer.
</Card>
<Card
title="Supabase database webhooks"
icon="book"
href="/guides/frameworks/supabase-edge-functions-database-webhooks"
>
Trigger tasks from Supabase database webhooks.
</Card>
</CardGroup>
## Benefits of using Trigger.dev for data processing & ETL workflows
**Process datasets for hours without timeouts:** Handle multi-hour transformations, large file processing, or complete database exports. No execution time limits.
**Parallel processing with built-in rate limiting:** Process thousands of records simultaneously while respecting API rate limits. Scale efficiently without overwhelming downstream services.
**Stream progress to your users in real-time:** Show row-by-row processing status updating live in your dashboard. Users see exactly where processing is and how long remains.
## Production use cases
<CardGroup cols={1}>
<Card title="MagicSchool AI customer story" href="https://trigger.dev/customers/magicschool-ai-customer-story">
Read how MagicSchool AI uses Trigger.dev to generate insights from millions of student interactions.
</Card>
<Card title="Comp AI customer story" href="https://trigger.dev/customers/comp-ai-customer-story">
Read how Comp AI uses Trigger.dev to automate evidence collection at scale, powering their open source, AI-driven compliance platform.
</Card>
<Card title="Midday customer story" href="https://trigger.dev/customers/midday-customer-story">
Read how Midday use Trigger.dev to sync large volumes of bank transactions in their financial management platform.
</Card>
</CardGroup>
## Example workflow patterns
<Tabs>
<Tab title="CSV file import">
Simple CSV import pipeline. Receives file upload, parses CSV rows, validates data, imports to database with progress tracking.
<div align="center">
```mermaid
graph TB
A[importCSV] --> B[parseCSVFile]
B --> C[validateRows]
C --> D[bulkInsertToDB]
D --> E[notifyCompletion]
```
</div>
</Tab>
<Tab title="Multi-source ETL pipeline">
**Coordinator pattern with parallel extraction**. Batch triggers parallel extraction from multiple sources (APIs, databases, S3), transforms and validates data, loads to data warehouse with monitoring.
<div align="center">
```mermaid
graph TB
A[runETLPipeline] --> B[coordinateExtraction]
B --> C[batchTriggerAndWait]
C --> D[extractFromAPI]
C --> E[extractFromDatabase]
C --> F[extractFromS3]
D --> G[transformData]
E --> G
F --> G
G --> H[validateData]
H --> I[loadToWarehouse]
```
</div>
</Tab>
<Tab title="Parallel web scraping">
**Coordinator pattern with browser automation**. Launches headless browsers in parallel to scrape multiple pages, extracts structured data, cleans and normalizes content, stores in database.
<div align="center">
```mermaid
graph TB
A[scrapeSite] --> B[coordinateScraping]
B --> C[batchTriggerAndWait]
C --> D[scrapePage1]
C --> E[scrapePage2]
C --> F[scrapePageN]
D --> G[cleanData]
E --> G
F --> G
G --> H[normalizeData]
H --> I[storeInDatabase]
```
</div>
</Tab>
<Tab title="Batch data enrichment">
**Coordinator pattern with rate limiting**. Fetches records needing enrichment, batch triggers parallel API calls with configurable concurrency to respect rate limits, validates enriched data, updates database.
<div align="center">
```mermaid
graph TB
A[enrichRecords] --> B[fetchRecordsToEnrich]
B --> C[coordinateEnrichment]
C --> D[batchTriggerAndWait]
D --> E[enrichRecord1]
D --> F[enrichRecord2]
D --> G[enrichRecordN]
E --> H[validateEnrichedData]
F --> H
G --> H
H --> I[updateDatabase]
```
</div>
</Tab>
</Tabs>
<UseCasesCards />
+147
View File
@@ -0,0 +1,147 @@
---
title: "Marketing workflows"
sidebarTitle: "Marketing"
description: "Learn how to use Trigger.dev for marketing workflows, including drip campaigns, behavioral triggers, personalization engines, and AI-powered content workflows"
---
import UseCasesCards from "/snippets/use-cases-cards.mdx";
## Overview
Build marketing workflows from email drip sequences to orchestrating full multi-channel campaigns. Handle multi-day sequences, behavioral triggers, dynamic content generation, and build live analytics dashboards.
## Featured examples
<CardGroup cols={3}>
<Card
title="Email sequences with Resend"
icon="book"
href="/guides/examples/resend-email-sequence"
>
Send multi-day email sequences with wait delays between messages.
</Card>
<Card
title="Product image generator"
icon="book"
href="/guides/example-projects/product-image-generator"
>
Transform product photos into professional marketing images using Replicate.
</Card>
<Card
title="Human-in-the-loop workflow"
icon="book"
href="/guides/example-projects/human-in-the-loop-workflow"
>
Approve marketing content using a human-in-the-loop workflow.
</Card>
</CardGroup>
## Benefits of using Trigger.dev for marketing workflows
**Delays without idle costs:** Wait hours or weeks between steps. Waits over 5 seconds are automatically checkpointed and don't count towards compute usage. Perfect for drip campaigns and scheduled follow-ups.
**Guaranteed delivery:** Messages send exactly once, even after retries. Personalized content isn't regenerated on failure.
**Scale without limits:** Process thousands in parallel while respecting rate limits. Send to entire segments without overwhelming APIs.
## Production use cases
<Card title="Icon customer story" href="https://trigger.dev/customers/icon-customer-story">
Read how Icon uses Trigger.dev to process and generate thousands of videos per month for their AI-driven video creation platform.
</Card>
## Example workflow patterns
<Tabs>
<Tab title="Drip email campaign">
Simple drip campaign. User signs up, waits specified delay, sends personalized email, tracks engagement.
<div align="center">
```mermaid
graph TB
A[userCreateAccount] --> B[sendWelcomeEmail]
B --> C[wait.for 24h]
C --> D[sendProductTipsEmail]
D --> E[wait.for 7d]
E --> F[sendFeedbackEmail]
```
</div>
</Tab>
<Tab title="Multi-channel campaigns">
**Router pattern with delay orchestration**. User action triggers campaign, router selects channel based on preferences (email/SMS/push), coordinates multi-day sequence with delays between messages, tracks engagement across channels.
<div align="center">
```mermaid
graph TB
A[startCampaign] --> B[fetchUserProfile]
B --> C[selectChannel]
C --> D{Preferred<br/>Channel?}
D -->|Email| E[sendEmail1]
D -->|SMS| F[sendSMS1]
D -->|Push| G[sendPush1]
E --> H[wait.for 2d]
F --> H
G --> H
H --> I[sendFollowUp]
I --> J[trackConversion]
```
</div>
</Tab>
<Tab title="AI content with approval">
**Supervisor pattern with approval gate**. Generates AI marketing content (images, copy, assets), pauses with wait.forToken for human review, applies revisions if needed, publishes to channels after approval.
<div align="center">
```mermaid
graph TB
A[createCampaignAssets] --> B[generateAIContent]
B --> C[wait.forToken approval]
C --> D{Approved?}
D -->|Yes| E[publishToChannels]
D -->|Needs revision| F[applyFeedback]
F --> B
```
</div>
</Tab>
<Tab title="Survey response enrichment">
**Coordinator pattern with enrichment**. User completes survey, batch triggers parallel enrichment from CRM/analytics, analyzes and scores responses, updates customer profiles, triggers personalized follow-up campaigns.
<div align="center">
```mermaid
graph TB
A[processSurveyResponse] --> B[coordinateEnrichment]
B --> C[batchTriggerAndWait]
C --> D[fetchCRMData]
C --> E[fetchAnalytics]
C --> F[fetchBehaviorData]
D --> G[analyzeAndScore]
E --> G
F --> G
G --> H[updateCRMProfile]
H --> I[triggerFollowUp]
```
</div>
</Tab>
</Tabs>
<UseCasesCards />
+144
View File
@@ -0,0 +1,144 @@
---
title: "AI media generation workflows"
sidebarTitle: "AI media generation"
description: "Learn how to use Trigger.dev for AI media generation including image creation, video synthesis, audio generation, and multi-modal content workflows"
---
import UseCasesCards from "/snippets/use-cases-cards.mdx";
## Overview
Build AI media generation pipelines that handle unpredictable API latencies and long-running operations. Generate images, videos, audio, and multi-modal content with automatic retries, progress tracking, and no timeout limits.
## Featured examples
<CardGroup cols={3}>
<Card
title="Product image generator"
icon="book"
href="/guides/example-projects/product-image-generator"
>
Transform product photos into professional marketing images using Replicate.
</Card>
<Card
title="Meme generator (human-in-the-loop)"
icon="book"
href="/guides/example-projects/meme-generator-human-in-the-loop"
>
Generate memes with DALL·E 3 and add human approval steps.
</Card>
<Card
title="Vercel AI SDK image generation"
icon="book"
href="/guides/example-projects/vercel-ai-sdk-image-generator"
>
Generate images from text prompts using the Vercel AI SDK.
</Card>
</CardGroup>
## Benefits of using Trigger.dev for AI media generation workflows
**Pay only for active compute, not AI inference time:** Checkpoint-resume pauses during AI API calls. Generate content that takes minutes or hours without paying for idle inference time.
**No timeout limits for long generations:** Handle generations that take minutes or hours without execution limits. Perfect for high-quality video synthesis and complex multi-modal workflows.
**Human approval gates for brand safety:** Add review steps before publishing AI-generated content. Pause workflows for human approval using waitpoint tokens.
## Production use cases
<CardGroup cols={1}>
<Card title="Icon customer story" href="https://trigger.dev/customers/icon-customer-story">
Read how Icon uses Trigger.dev to process and generate thousands of videos per month for their AI-driven video creation platform.
</Card>
<Card title="Papermark customer story" href="https://trigger.dev/customers/papermark-customer-story">
Read how Papermark process thousands of documents per month using Trigger.dev.
</Card>
</CardGroup>
## Example workflow patterns
<Tabs>
<Tab title="AI content with approval">
**Supervisor pattern with approval gate**. Generates AI content, pauses execution with wait.forToken to allow human review, applies feedback if needed, publishes approved content.
<div align="center">
```mermaid
graph TB
A[generateContent] --> B[createWithAI]
B --> C[wait.forToken approval]
C --> D{Approved?}
D -->|Yes| E[publishContent]
D -->|Needs revision| F[applyFeedback]
F --> B
```
</div>
</Tab>
<Tab title="AI image generation">
Simple AI image generation. Receives prompt and parameters, calls OpenAI DALL·E 3, post-processes result, uploads to storage.
<div align="center">
```mermaid
graph TB
A[generateImage] --> B[optimizeImage]
B --> C[uploadToStorage]
C --> D[updateDatabase]
```
</div>
</Tab>
<Tab title="Batch image generation">
**Coordinator pattern with rate limiting**. Receives batch of generation requests, coordinates parallel processing with configurable concurrency to respect API rate limits, validates outputs, stores results.
<div align="center">
```mermaid
graph TB
A[processBatch] --> B[coordinateGeneration]
B --> C[batchTriggerAndWait]
C --> D[generateImage1]
C --> E[generateImage2]
C --> F[generateImageN]
D --> G[validateResults]
E --> G
F --> G
G --> H[storeResults]
H --> I[notifyCompletion]
```
</div>
</Tab>
<Tab title="Multi-step image enhancement">
**Coordinator pattern with sequential processing**. Generates initial content with AI, applies style transfer or enhancement, upscales resolution, optimizes and compresses for delivery.
<div align="center">
```mermaid
graph TB
A[processCreative] --> B[generateWithAI]
B --> C[applyStyleTransfer]
C --> D[upscaleResolution]
D --> E[optimizeAndCompress]
E --> F[uploadToStorage]
```
</div>
</Tab>
</Tabs>
<UseCasesCards />
+191
View File
@@ -0,0 +1,191 @@
---
title: "Media processing workflows"
sidebarTitle: "Media processing"
description: "Learn how to use Trigger.dev for media processing including video transcoding, image optimization, audio transformation, and document conversion."
---
import UseCasesCards from "/snippets/use-cases-cards.mdx";
## Overview
Build media processing pipelines that handle large files and long-running operations. Process videos, images, audio, and documents with automatic retries, progress tracking, and no timeout limits.
## Featured examples
<CardGroup cols={3}>
<Card title="FFmpeg video processing" icon="book" href="/guides/examples/ffmpeg-video-processing">
Process videos and upload results to R2 storage using FFmpeg.
</Card>
<Card
title="Product image generator"
icon="book"
href="/guides/example-projects/product-image-generator"
>
Transform product photos into professional marketing images using Replicate.
</Card>
<Card
title="LibreOffice PDF conversion"
icon="book"
href="/guides/examples/libreoffice-pdf-conversion"
>
Convert documents to PDF using LibreOffice.
</Card>
</CardGroup>
## Benefits of using Trigger.dev for media processing workflows
**Process multi-hour videos without timeouts:** Transcode videos, extract frames, or run CPU-intensive operations for hours. No execution time limits.
**Stream progress to users in real-time:** Show processing status updating live in your UI. Users see exactly where encoding is and how long remains.
**Parallel processing with resource control:** Process hundreds of files simultaneously with configurable concurrency limits. Control resource usage without overwhelming infrastructure.
## Example workflow patterns
<Tabs>
<Tab title="Video transcode">
Simple video transcoding pipeline. Downloads video from storage, batch triggers parallel transcoding to multiple formats and thumbnail extraction, uploads all results.
<div align="center">
```mermaid
graph TB
A[processVideo] --> B[downloadFromStorage]
B --> C[batchTriggerAndWait]
C --> D[transcodeToHD]
C --> E[transcodeToSD]
C --> F[extractThumbnail]
D --> G[uploadToStorage]
E --> G
F --> G
```
</div>
</Tab>
<Tab title="Adaptive video processing">
**Router + Coordinator pattern**. Analyzes video metadata to determine source resolution, routes to appropriate transcoding preset, batch triggers parallel post-processing for thumbnails, preview clips, and chapter detection.
<div align="center">
```mermaid
graph TB
A[processVideoUpload] --> B[analyzeMetadata]
B --> C{Source<br/>Resolution?}
C -->|4K Source| D[transcode4K]
C -->|HD Source| E[transcodeHD]
C -->|SD Source| F[transcodeSD]
D --> G[coordinatePostProcessing]
E --> G
F --> G
G --> H[batchTriggerAndWait]
H --> I[extractThumbnails]
H --> J[generatePreview]
H --> K[detectChapters]
I --> L[uploadToStorage]
J --> L
K --> L
L --> M[notifyComplete]
```
</div>
</Tab>
<Tab title="Smart image optimization">
**Router + Coordinator pattern**. Analyzes image content to detect type, routes to specialized processing (background removal for products, face detection for portraits, scene analysis for landscapes), upscales with AI, batch triggers parallel variant generation.
<div align="center">
```mermaid
graph TB
A[processImageUpload] --> B[analyzeContent]
B --> C{Content<br/>Type?}
C -->|Product| D[removeBackground]
C -->|Portrait| E[detectFaces]
C -->|Landscape| F[analyzeScene]
D --> G[upscaleWithAI]
E --> G
F --> G
G --> H[batchTriggerAndWait]
H --> I[generateWebP]
H --> J[generateThumbnails]
H --> K[generateSocialCrops]
I --> L[uploadToStorage]
J --> L
K --> L
```
</div>
</Tab>
<Tab title="Podcast production">
**Coordinator pattern**. Pre-processes raw audio with noise reduction and speaker diarization, batch triggers parallel tasks for transcription (Deepgram), audio enhancement, and chapter detection, aggregates results to generate show notes and publish.
<div align="center">
```mermaid
graph TB
A[processAudioUpload] --> B[cleanAudio]
B --> C[coordinateProcessing]
C --> D[batchTriggerAndWait]
D --> E[transcribeWithDeepgram]
D --> F[enhanceAudio]
D --> G[detectChapters]
E --> H[generateShowNotes]
F --> H
G --> H
H --> I[publishToPlatforms]
```
</div>
</Tab>
<Tab title="Document extraction with approval">
**Router pattern with human-in-the-loop**. Detects file type and routes to appropriate processor, classifies document with AI to determine type (invoice/contract/receipt), extracts structured data fields, optionally pauses with wait.forToken for human approval.
<div align="center">
```mermaid
graph TB
A[processDocumentUpload] --> B[detectFileType]
B -->|PDF| C[extractText]
B -->|Word/Excel| D[convertToPDF]
B -->|Image| E[runOCR]
C --> F[classifyDocument]
D --> F
E --> F
F -->|Invoice| G[extractLineItems]
F -->|Contract| H[extractClauses]
F -->|Receipt| I[extractExpenses]
G --> J{Needs<br/>Review?}
H --> J
I --> J
J -->|Yes| K[wait.forToken approval]
J -->|No| L[processAndIntegrate]
K --> L
```
</div>
</Tab>
</Tabs>
<UseCasesCards />
+11
View File
@@ -0,0 +1,11 @@
---
title: "Use cases"
sidebarTitle: "Overview"
description: "Explore common use cases for Trigger.dev including data processing, media workflows, marketing automation, and AI generation"
---
import UseCasesCards from "/snippets/use-cases-cards.mdx";
Trigger.dev handles workflows that traditional platforms struggle with: long-running operations, unpredictable API latencies, multi-hour processing, and complex orchestration patterns. Our platform provides no timeout limits, automatic retries, and real-time progress tracking built in.
<UseCasesCards />
+353
View File
@@ -0,0 +1,353 @@
---
title: "Upgrading from v2"
description: "How to upgrade v2 jobs to v3 tasks, and how to use them together."
---
## Changes from v2 to v3
The main difference is that things in v3 are far simpler. That's because in v3 your code is deployed to our servers (unless you self-host) which are long-running.
1. No timeouts.
2. No `io.runTask()` (and no `cacheKeys`).
3. Just use official SDKs, not integrations.
4. `task`s are the new primitive, not `job`s.
## Convert your v2 job using an AI prompt
The prompt in the accordion below gives good results when using Anthropic Claude 3.5 Sonnet. Youll need a relatively large token limit.
<Note>Don't forget to paste your own v2 code in a markdown codeblock at the bottom of the prompt before running it.</Note>
<Accordion title="Copy and paste this prompt in full:">
I would like you to help me convert from Trigger.dev v2 to Trigger.dev v3.
The important differences:
1. The syntax for creating "background jobs" has changed. In v2 it looked like this:
```ts
import { eventTrigger } from "@trigger.dev/sdk";
import { client } from "@/trigger";
import { db } from "@/lib/db";
client.defineJob({
enabled: true,
id: "my-job-id",
name: "My job name",
version: "0.0.1",
// This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction
trigger: eventTrigger({
name: "theevent.name",
schema: z.object({
phoneNumber: z.string(),
verified: z.boolean(),
}),
}),
run: async (payload, io) => {
//everything needed to be wrapped in io.runTask in v2, to make it possible for long-running code to work
const result = await io.runTask("get-stuff-from-db", async () => {
const socials = await db.query.Socials.findMany({
where: eq(Socials.service, "tiktok"),
});
return socials;
});
io.logger.info("Completed fetch successfully");
},
});
```
In v3 it looks like this:
```ts
import { task } from "@trigger.dev/sdk";
import { db } from "@/lib/db";
export const getCreatorVideosFromTikTok = task({
id: "my-job-id",
run: async (payload: { phoneNumber: string, verified: boolean }) => {
//in v3 there are no timeouts, so you can just use the code as is, no need to wrap in `io.runTask`
const socials = await db.query.Socials.findMany({
where: eq(Socials.service, "tiktok"),
});
//use `logger` instead of `io.logger`
logger.info("Completed fetch successfully");
},
});
```
Notice that the schema on v2 `eventTrigger` defines the payload type. In v3 that needs to be done on the TypeScript type of the `run` payload param.
2. v2 had integrations with some APIs. Any package that isn't `@trigger.dev/sdk` can be replaced with an official SDK. The syntax may need to be adapted.
For example:
v2:
```ts
import { OpenAI } from "@trigger.dev/openai";
const openai = new OpenAI({
id: "openai",
apiKey: process.env.OPENAI_API_KEY!,
});
client.defineJob({
id: "openai-job",
name: "OpenAI Job",
version: "1.0.0",
trigger: invokeTrigger(),
integrations: {
openai, // Add the OpenAI client as an integration
},
run: async (payload, io, ctx) => {
// Now you can access it through the io object
const completion = await io.openai.chat.completions.create("completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
},
});
```
Would become in v3:
```ts
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
export const openaiJob = task({
id: "openai-job",
run: async (payload) => {
const completion = await openai.chat.completions.create(
{
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
},
});
```
So don't use the `@trigger.dev/openai` package in v3, use the official OpenAI SDK.
Bear in mind that the syntax for the latest official SDK will probably be different from the @trigger.dev integration SDK. You will need to adapt the code accordingly.
3. The most critical difference is that inside the `run` function you do NOT need to wrap everything in `io.runTask`. So anything inside there can be extracted out and be used in the main body of the function without wrapping it.
4. The import for `task` in v3 is `import { task } from "@trigger.dev/sdk";`
5. You can trigger jobs from other jobs. In v2 this was typically done by either calling `io.sendEvent()` or by calling `yourOtherTask.invoke()`. In v3 you call `.trigger()` on the other task, there are no events in v3.
v2:
```ts
export const parentJob = client.defineJob({
id: "parent-job",
run: async (payload, io) => {
//send event
await client.sendEvent({
name: "user.created",
payload: { name: "John Doe", email: "john@doe.com", paidPlan: true },
});
//invoke
await exampleJob.invoke({ foo: "bar" }, {
idempotencyKey: `some_string_here_${
payload.someValue
}_${new Date().toDateString()}`,
});
},
});
```
v3:
```ts
export const parentJob = task({
id: "parent-job",
run: async (payload) => {
//trigger
await userCreated.trigger({ name: "John Doe", email: "john@doe.com", paidPlan: true });
//trigger, you can pass in an idempotency key
await exampleJob.trigger({ foo: "bar" }, {
idempotencyKey: `some_string_here_${
payload.someValue
}_${new Date().toDateString()}`,
});
}
});
```
Can you help me convert the following code from v2 to v3? Please include the full converted code in the answer, do not truncate it anywhere.
</Accordion>
## OpenAI example comparison
This is a (very contrived) example that does a long OpenAI API call (>10s), stores the result in a database, waits for 5 mins, and then returns the result.
### v2
First, the old v2 code, which uses the OpenAI integration. Comments inline:
```ts v2 OpenAI task
import { client } from "~/trigger";
import { eventTrigger } from "@trigger.dev/sdk";
//1. A Trigger.dev integration for OpenAI
import { OpenAI } from "@trigger.dev/openai";
const openai = new OpenAI({
id: "openai",
apiKey: process.env["OPENAI_API_KEY"]!,
});
//2. Use the client to define a "Job"
client.defineJob({
id: "openai-tasks",
name: "OpenAI Tasks",
version: "0.0.1",
trigger: eventTrigger({
name: "openai.tasks",
schema: z.object({
prompt: z.string(),
}),
}),
//3. integrations are added and come through to `io` in the run fn
integrations: {
openai,
},
run: async (payload, io, ctx) => {
//4. You use `io` to get the integration
//5. Also note that "backgroundCreate" was needed for OpenAI
// to do work that lasted longer than your serverless timeout
const chatCompletion = await io.openai.chat.completions.backgroundCreate(
//6. You needed to add "cacheKeys" to any "task"
"background-chat-completion",
{
messages: [{ role: "user", content: payload.prompt }],
model: "gpt-3.5-turbo",
}
);
const result = chatCompletion.choices[0]?.message.content;
if (!result) {
//7. throwing an error at the top-level in v2 failed the task immediately
throw new Error("No result from OpenAI");
}
//8. io.runTask needed to be used to prevent work from happening twice
const dbRow = await io.runTask("store-in-db", async (task) => {
//9. Custom logic can be put here
// Anything returned must be JSON-serializable, so no Date objects etc.
return saveToDb(result);
});
//10. Wait for 5 minutes.
// You need a cacheKey and the 2nd param is a number
await io.wait("wait some time", 60 * 5);
//11. Anything returned must be JSON-serializable, so no Date objects etc.
return result;
},
});
```
### v3
In v3 we eliminate a lot of code mainly because we don't need tricks to try avoid timeouts. Here's the equivalent v3 code:
```ts v3 OpenAI task
import { logger, task, wait } from "@trigger.dev/sdk";
//1. Official OpenAI SDK
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
//2. Jobs don't exist now, use "task"
export const openaiTask = task({
id: "openai-task",
//3. Retries happen if a task throws an error that isn't caught
// The default settings are in your trigger.config.ts (used if not overriden here)
retry: {
maxAttempts: 3,
},
run: async (payload: { prompt: string }) => {
//4. Use the official SDK
//5. No timeouts, so this can take a long time
const chatCompletion = await openai.chat.completions.create({
messages: [{ role: "user", content: payload.prompt }],
model: "gpt-3.5-turbo",
});
const result = chatCompletion.choices[0]?.message.content;
if (!result) {
//6. throwing an error at the top-level will retry the task (if retries are enabled)
throw new Error("No result from OpenAI");
}
//7. No need to use runTask, just call the function
const dbRow = await saveToDb(result);
//8. You can provide seconds, minutes, hours etc.
// You don't need cacheKeys in v3
await wait.for({ minutes: 5 });
//9. You can return anything that's serializable using SuperJSON
// That includes undefined, Date, bigint, RegExp, Set, Map, Error and URL.
return result;
},
});
```
## Triggering tasks comparison
### v2
In v2 there were different trigger types and triggering each type was slightly different.
```ts v2 triggering
async function yourBackendFunction() {
//1. for `eventTrigger` you use `client.sendEvent`
const event = await client.sendEvent({
name: "openai.tasks",
payload: { prompt: "Create a good programming joke about background jobs" },
});
//2. for `invokeTrigger` you'd call `invoke` on the job
const { id } = await invocableJob.invoke({
prompt: "What is the meaning of life?",
});
}
```
### v3
We've unified triggering in v3. You use `trigger()` or `batchTrigger()` which you can do on any type of task. Including scheduled, webhooks, etc if you want.
```ts v3 triggering
async function yourBackendFunction() {
//call `trigger()` on any task
const handle = await openaiTask.trigger({
prompt: "Tell me a programming joke",
});
}
```
## Upgrading your project
1. Make sure to upgrade all of your trigger.dev packages to v3 first.
```bash
npx @trigger.dev/cli@latest update --to 3.0.0
```
2. Follow the [v3 quick start](/quick-start) to get started with v3. Our new CLI will take care of the rest.
## Using v2 together with v3
You can use v2 and v3 in the same codebase. This can be useful where you already have v2 jobs or where we don't support features you need (yet).
<Note>We do not support calling v3 tasks from v2 jobs or vice versa.</Note>