Skip to content

Commit 684024b

Browse files
committed
How it works doc
1 parent 0b25c21 commit 684024b

File tree

2 files changed

+140
-76
lines changed

2 files changed

+140
-76
lines changed

docs/how-it-works.mdx

Lines changed: 140 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -4,83 +4,79 @@ sidebarTitle: "How it works"
44
description: "Understand how Trigger.dev works and how it can help you."
55
---
66

7+
## Introduction
8+
79
Trigger.dev v3 allows you to embed long-running async tasks into your application and run them in the background. This allows you to offload tasks that take a long time to complete, such as sending emails, processing videos, or running long chains of AI tasks.
810

911
For example, the below task processes a video with `ffmpeg` and sends the results to an s3 bucket, then updates a database with the results and sends an email to the user.
1012

1113
```ts /trigger/video.ts
12-
import { task, logger } from "@trigger.dev/sdk/v3";
14+
import { logger, task } from "@trigger.dev/sdk/v3";
15+
import { updateVideoUrl } from "../db.js";
1316
import ffmpeg from "fluent-ffmpeg";
1417
import { Readable } from "node:stream";
18+
import type { ReadableStream } from "node:stream/web";
1519
import * as fs from "node:fs/promises";
1620
import * as path from "node:path";
1721
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
18-
import { updateDatabase } from "./database";
19-
import { sendEmail } from "./email";
22+
import { sendEmail } from "../email.js";
23+
import { getVideo } from "../db.js";
2024

2125
// Initialize S3 client
22-
const s3Client = new S3Client({ region: process.env.AWS_REGION }); // Replace with your preferred region
26+
const s3Client = new S3Client({
27+
region: process.env.AWS_REGION,
28+
});
2329

2430
export const convertVideo = task({
2531
id: "convert-video",
26-
run: async (payload: { videoUrl: string; videoId: string; userId: string }) => {
27-
const { videoUrl, videoId, userId } = payload;
32+
retry: {
33+
maxAttempts: 5,
34+
minTimeoutInMs: 1000,
35+
maxTimeoutInMs: 10000,
36+
factor: 2,
37+
},
38+
run: async ({ videoId }: { videoId: string }) => {
39+
const { url, userId } = await getVideo(videoId);
2840

29-
const outputPath = path.join("/tmp", `output_${Date.now()}.mp4`);
41+
const outputPath = path.join("/tmp", `output_${videoId}.mp4`);
3042

31-
// Fetch the video
32-
const response = await fetch(videoUrl);
43+
const response = await fetch(url);
3344

34-
// Process the video
3545
await new Promise((resolve, reject) => {
36-
ffmpeg(Readable.fromWeb(response.body))
37-
.videoFilters("scale=iw/2:ih/2") // This halves both width and height
46+
ffmpeg(Readable.fromWeb(response.body as ReadableStream))
47+
.videoFilters("scale=iw/2:ih/2")
3848
.output(outputPath)
3949
.on("end", resolve)
4050
.on("error", reject)
4151
.run();
4252
});
4353

44-
// Upload the video to S3
45-
const s3Key = `processed-videos/${path.basename(outputPath)}`;
46-
47-
try {
48-
const fileContent = await fs.readFile(outputPath);
49-
const uploadParams = {
50-
Bucket: process.env.S3_BUCKET,
51-
Key: s3Key,
52-
Body: fileContent,
53-
};
54-
55-
await s3Client.send(new PutObjectCommand(uploadParams));
56-
57-
logger.info("Video uploaded successfully to S3");
54+
const processedContent = await fs.readFile(outputPath);
5855

59-
// Generate the S3 URL
60-
const s3Url = `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${s3Key}`;
56+
// Upload to S3
57+
const s3Key = `processed-videos/output_${videoId}.mp4`;
6158

62-
// Update the database with the S3 URL
63-
await updateDatabase(videoId, s3Url);
59+
const uploadParams = {
60+
Bucket: process.env.S3_BUCKET,
61+
Key: s3Key,
62+
Body: processedContent,
63+
};
6464

65-
logger.info("Database updated with S3 URL");
65+
await s3Client.send(new PutObjectCommand(uploadParams));
66+
const s3Url = `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${s3Key}`;
6667

67-
// Send an email to the user
68-
await sendEmail(
69-
userId,
70-
"Video Processing Complete",
71-
`Your video has been processed and is available at: ${s3Url}`
72-
);
68+
logger.info("Video converted", { videoId, s3Url });
7369

74-
logger.info("Email sent to user");
70+
// Update database
71+
await updateVideoUrl(videoId, s3Url);
7572

76-
// Clean up the temporary file
77-
await fs.unlink(outputPath);
73+
await sendEmail(
74+
userId,
75+
"Video Processing Complete",
76+
`Your video has been processed and is available at: ${s3Url}`
77+
);
7878

79-
return { success: true, s3Url };
80-
} catch (error) {
81-
logger.error("Error processing video:", error);
82-
throw error;
83-
}
79+
return { success: true, s3Url };
8480
},
8581
});
8682
```
@@ -96,6 +92,7 @@ import type { convertVideo } from "./trigger/video";
9692
export async function POST(request: Request) {
9793
const body = await request.json();
9894

95+
// Trigger the task, this will return before the task is completed
9996
const handle = await tasks.trigger<typeof convertVideo>("convert-video", body);
10097

10198
return NextResponse.json(handle);
@@ -104,6 +101,25 @@ export async function POST(request: Request) {
104101

105102
This will schedule the task to run in the background and return a handle that you can use to check the status of the task. This allows your backend application to respond quickly to the user and offload the long-running task to Trigger.dev.
106103

104+
## The CLI
105+
106+
Trigger.dev comes with a CLI that allows you to initialize Trigger.dev into your project, deploy your tasks, and run your tasks locally. You can run it via `npx` like so:
107+
108+
```sh
109+
npx trigger.dev@latest login # Log in to your Trigger.dev account
110+
npx trigger.dev@latest init # Initialize Trigger.dev in your project
111+
npx trigger.dev@latest dev # Run your tasks locally
112+
npx trigger.dev@latest deploy # Deploy your tasks to the Trigger.dev instance
113+
```
114+
115+
All these commands work with the Trigger.dev cloud and/or your self-hosted instance. It supports multiple profiles so you can easily switch between different accounts or instances.
116+
117+
```sh
118+
npx trigger.dev@latest login --profile <profile> -a https://trigger.example.com # Log in to a specific profile into a self-hosted instance
119+
npx trigger.dev@latest dev --profile <profile> # Initialize Trigger.dev in your project
120+
npx trigger.dev@latest deploy --profile <profile> # Deploy your tasks to the Trigger.dev instance
121+
```
122+
107123
## Trigger.dev architecture
108124

109125
Trigger.dev implements a serverless architecture (without timeouts!) that allows you to run your tasks in a scalable and reliable way. When you run `npx trigger.dev@latest deploy`, we build and deploy your task code to your Trigger.dev instance. Then, when you trigger a task from your application, it's run in a secure, isolated environment with the resources you need to complete the task. A simplified diagram for a task execution looks like this:
@@ -233,36 +249,37 @@ Let's rewrite the `convert-video` task above to be more durable:
233249
<CodeGroup>
234250

235251
```ts /trigger/video.ts
236-
import { task, logger } from "@trigger.dev/sdk/v3";
237-
import { processVideo, uploadToS3, sendUserEmail } from "./tasks.js";
238-
import { updateDatabase } from "./database.js";
252+
import { idempotencyKeys, logger, task } from "@trigger.dev/sdk/v3";
253+
import { processVideo, sendUserEmail, uploadToS3 } from "./tasks.js";
254+
import { updateVideoUrl } from "../db.js";
239255

240256
export const convertVideo = task({
241257
id: "convert-video",
242-
retries: {
243-
maxAttempts: 10,
258+
retry: {
259+
maxAttempts: 5,
244260
minTimeoutInMs: 1000,
245261
maxTimeoutInMs: 10000,
246262
factor: 2,
247263
},
248-
run: async (payload: { videoUrl: string; videoId: string; userId: string }) => {
249-
const { videoUrl, userId, videoId } = payload;
264+
run: async ({ videoId }: { videoId: string }) => {
265+
// Automatically scope the idempotency key to this run, across retries
266+
const idempotencyKey = await idempotencyKeys.create(videoId);
250267

251268
// Process video
252269
const { processedContent } = await processVideo
253-
.triggerAndWait({ videoUrl, videoId }, { idempotencyKey: ["process", videoId] })
270+
.triggerAndWait({ videoId }, { idempotencyKey })
254271
.unwrap(); // Calling unwrap will return the output of the subtask, or throw an error if the subtask failed
255272

256273
// Upload to S3
257274
const { s3Url } = await uploadToS3
258-
.triggerAndWait({ processedContent, videoId }, { idempotencyKey: ["upload", videoId] })
275+
.triggerAndWait({ processedContent, videoId }, { idempotencyKey })
259276
.unwrap();
260277

261278
// Update database
262-
await updateDatabase(videoId, s3Url);
279+
await updateVideoUrl(videoId, s3Url);
263280

264281
// Send email, we don't need to wait for this to finish
265-
await sendUserEmail.trigger({ userId, s3Url }, { idempotencyKey: ["email", videoId] });
282+
await sendUserEmail.trigger({ videoId, s3Url }, { idempotencyKey });
266283

267284
return { success: true, s3Url };
268285
},
@@ -273,32 +290,40 @@ export const convertVideo = task({
273290
import { task, logger } from "@trigger.dev/sdk/v3";
274291
import ffmpeg from "fluent-ffmpeg";
275292
import { Readable } from "node:stream";
293+
import type { ReadableStream } from "node:stream/web";
276294
import * as fs from "node:fs/promises";
277295
import * as path from "node:path";
278296
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
279-
import { sendEmail } from "./email";
297+
import { sendEmail } from "../email.js";
298+
import { getVideo } from "../db.js";
280299

281300
// Initialize S3 client
282-
const s3Client = new S3Client({ region: process.env.AWS_REGION });
301+
const s3Client = new S3Client({
302+
region: process.env.AWS_REGION,
303+
});
283304

284305
export const processVideo = task({
285306
id: "process-video",
286-
run: async (payload: { videoUrl: string; videoId: string }) => {
287-
const { videoUrl, videoId } = payload;
288-
const outputPath = path.join("/tmp", `output_${videoId}.mp4`);
289-
const response = await fetch(videoUrl);
307+
run: async ({ videoId }: { videoId: string }) => {
308+
const { url } = await getVideo(videoId);
290309

291-
await new Promise((resolve, reject) => {
292-
ffmpeg(Readable.fromWeb(response.body))
293-
.videoFilters("scale=iw/2:ih/2")
294-
.output(outputPath)
295-
.on("end", resolve)
296-
.on("error", reject)
297-
.run();
310+
const outputPath = path.join("/tmp", `output_${videoId}.mp4`);
311+
const response = await fetch(url);
312+
313+
await logger.trace("ffmpeg", async (span) => {
314+
await new Promise((resolve, reject) => {
315+
ffmpeg(Readable.fromWeb(response.body as ReadableStream))
316+
.videoFilters("scale=iw/2:ih/2")
317+
.output(outputPath)
318+
.on("end", resolve)
319+
.on("error", reject)
320+
.run();
321+
});
298322
});
299323

300324
const processedContent = await fs.readFile(outputPath);
301-
await fs.unlink(outputPath); // Clean up the temporary file
325+
326+
await fs.unlink(outputPath);
302327

303328
return { processedContent: processedContent.toString("base64") };
304329
},
@@ -308,7 +333,9 @@ export const uploadToS3 = task({
308333
id: "upload-to-s3",
309334
run: async (payload: { processedContent: string; videoId: string }) => {
310335
const { processedContent, videoId } = payload;
336+
311337
const s3Key = `processed-videos/output_${videoId}.mp4`;
338+
312339
const uploadParams = {
313340
Bucket: process.env.S3_BUCKET,
314341
Key: s3Key,
@@ -324,9 +351,10 @@ export const uploadToS3 = task({
324351

325352
export const sendUserEmail = task({
326353
id: "send-user-email",
327-
run: async (payload: { userId: string; s3Url: string }) => {
328-
const { userId, s3Url } = payload;
329-
await sendEmail(
354+
run: async ({ videoId, s3Url }: { videoId: string; s3Url: string }) => {
355+
const { userId } = await getVideo(videoId);
356+
357+
return await sendEmail(
330358
userId,
331359
"Video Processing Complete",
332360
`Your video has been processed and is available at: ${s3Url}`
@@ -374,14 +402,50 @@ sequenceDiagram
374402

375403
## The build system
376404

377-
When you run `npx trigger.dev@latest deploy` or `npx trigger.dev@latest dev`, we build your task code using our build system, which is powered by esbuild. When deploying, the code is packaged up into a Docker image and deployed to your Trigger.dev instance. When running in development mode, the code is built and run locally on your machine. Some features of our build system include:
405+
When you run `npx trigger.dev@latest deploy` or `npx trigger.dev@latest dev`, we build your task code using our build system, which is powered by [esbuild](https://esbuild.github.io/). When deploying, the code is packaged up into a Docker image and deployed to your Trigger.dev instance. When running in dev mode, the code is built and run locally on your machine. Some features of our build system include:
378406

379407
- **Bundled by default**: Code + dependencies are bundled and tree-shaked by default.
380408
- **Build extensions**: Use and write custom build extensions to transform your code or the resulting docker image.
381409
- **ESM ouput**: We output to ESM, which allows tree-shaking and better performance.
382410

383-
You can learn more about working with our build system in the [configuration docs](/config/config-file).
411+
You can review the build output by running deploy with the `--dry-run` flag, which will output the Containerfile and the build output.
412+
413+
Learn more about working with our build system in the [configuration docs](/config/config-file).
414+
415+
## Dev mode
416+
417+
When you run `npx trigger.dev@latest dev`, we run your task code locally on your machine. All scheduling is still done in the Trigger.dev server instance, but the task code is run locally. This allows you to develop and test your tasks locally before deploying them to the cloud, and is especially useful for debugging and testing.
418+
419+
- The same build system is used in dev mode, so you can be sure that your code will run the same locally as it does in the cloud.
420+
- Changes are automatically detected and a new version is spun up when you save your code.
421+
- Add debuggers and breakpoints to your code and debug it locally.
422+
- Each task is run in a separate process, so you can run multiple tasks in parallel.
423+
- Auto-cancels tasks when you stop the dev server.
424+
425+
<Note>
426+
Trigger.dev currently does not support "offline" dev mode, where you can run tasks without an
427+
internet connection. Please let us know if this is a feature you want/need.
428+
</Note>
429+
430+
## Staging and production environments
431+
432+
Trigger.dev supports deploying to multiple "deployed" environments, such as staging and production. This allows you to test your tasks in a staging environment before deploying them to production. You can deploy to a new environment by running `npx trigger.dev@latest deploy --env <env>`, where `<env>` is the name of the environment you want to deploy to. Each environment has its own API Key, which you can use to trigger tasks in that environment.
384433

385434
## OpenTelemetry
386435

387-
The Trigger.dev logging and task dashboard is powered by OpenTelemetry traces and logs, which allows you to trace your tasks and auto-instrument your code.
436+
The Trigger.dev logging and task dashboard is powered by OpenTelemetry traces and logs, which allows you to trace your tasks and auto-instrument your code. We also auto-correlate logs from subtasks and parent tasks, making it easy view the entire trace of a task execution. A single run of the video processing task above looks like this in the dashboard:
437+
438+
![OpenTelemetry trace](/images/opentelemetry-trace.png)
439+
440+
Because we use standard OpenTelemetry, you can instrument your code and OpenTelemetry compatible libraries to get detailed traces and logs of your tasks. The above trace instruments both Prisma and the AWS SDK:
441+
442+
```ts trigger.config.ts
443+
import { defineConfig } from "@trigger.dev/sdk/v3";
444+
import { PrismaInstrumentation } from "@prisma/instrumentation";
445+
import { AwsInstrumentation } from "@opentelemetry/instrumentation-aws-sdk";
446+
447+
export default defineConfig({
448+
project: "<your-project-ref>",
449+
instrumentations: [new PrismaInstrumentation(), new AwsInstrumentation()],
450+
});
451+
```

docs/images/opentelemetry-trace.png

498 KB
Loading

0 commit comments

Comments
 (0)