Skip to content

Commit 2637e47

Browse files
authored
docs: add migration guide from n8n to Trigger.dev (#3283)
Adds a migration reference for users moving from n8n to Trigger.dev. Includes a concept map, four common patterns covering the migration-specific gaps, and a full customer onboarding example. The onboarding workflow highlights the 3-day wait pattern, an area where n8n's execution model has known reliability issues at production scale that Trigger.dev handles natively
1 parent 0977c56 commit 2637e47

File tree

2 files changed

+346
-4
lines changed

2 files changed

+346
-4
lines changed

docs/docs.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,10 @@
398398
}
399399
]
400400
},
401+
{
402+
"group": "Migration guides",
403+
"pages": ["migration-mergent", "migration-n8n"]
404+
},
401405
{
402406
"group": "Use cases",
403407
"pages": [
@@ -468,10 +472,6 @@
468472
"guides/examples/vercel-sync-env-vars"
469473
]
470474
},
471-
{
472-
"group": "Migration guides",
473-
"pages": ["migration-mergent"]
474-
},
475475
{
476476
"group": "Community packages",
477477
"pages": [

docs/migration-n8n.mdx

Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
---
2+
title: "Migrating from n8n"
3+
description: "A practical guide for moving your n8n workflows to Trigger.dev"
4+
sidebarTitle: "Migrating from n8n"
5+
---
6+
7+
If you've been building with n8n and are ready to move to code-first workflows, this guide is for you. This page maps them to their Trigger.dev equivalents and walks through common patterns side by side.
8+
9+
Your task code runs on Trigger.dev's managed infrastructure, so there are no servers for you to provision or maintain.
10+
11+
## Concept map
12+
13+
| n8n | Trigger.dev |
14+
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
15+
| Workflow | [task](/tasks/overview) plus its config (queue, retry, onFailure) |
16+
| Schedule Trigger | [schedules.task](/tasks/scheduled) |
17+
| Webhook node | Route handler + [task.trigger()](/triggering) |
18+
| Node | A step or library call inside `run()` |
19+
| Execute Sub-workflow node (wait for completion) | [tasks.triggerAndWait()](/triggering#yourtask-triggerandwait) |
20+
| Execute Sub-workflow node (execute in background) | [tasks.trigger()](/triggering) |
21+
| Loop over N items → Execute Sub-workflow → Merge | [tasks.batchTriggerAndWait()](/tasks#yourtask-batchtriggerandwait) |
22+
| Loop Over Items (Split in Batches) | `for` loop or `.map()` |
23+
| IF / Switch node | `if` / `switch` statements |
24+
| Wait node (time interval or specific time) | [wait.for()](/wait-for) or [wait.until()](/wait-until) |
25+
| Error Trigger node / Error Workflow | [onFailure](/tasks/overview#onfailure-function) hook (both collapse into one concept in Trigger.dev) |
26+
| Continue On Fail | `try/catch` around an individual step |
27+
| Stop And Error | `throw new Error(...)` |
28+
| Code node | A function or step within `run()` |
29+
| Credentials | [Environment variable secret](/deploy-environment-variables) |
30+
| Execution | Run (visible in the dashboard with full logs) |
31+
| Retry on Fail (per-node setting) | [retry.maxAttempts](/tasks/overview#retry) (retries the whole `run()`, not a single step) |
32+
| AI Agent node | Any AI SDK called inside `run()` (Vercel AI SDK, Claude SDK, OpenAI SDK, etc.) |
33+
| Respond to Webhook node | Route handler + [task.triggerAndWait()](/triggering#yourtask-triggerandwait) returning the result as HTTP response |
34+
35+
---
36+
37+
## Setup
38+
39+
<Steps>
40+
41+
<Step title="Create an account">
42+
43+
Go to [Trigger.dev Cloud](https://cloud.trigger.dev), create an account, and create a project.
44+
45+
</Step>
46+
47+
<Step title="Install the CLI and initialize">
48+
49+
```bash
50+
npx trigger.dev@latest init
51+
```
52+
53+
This adds Trigger.dev to your project and creates a `trigger/` directory for your tasks.
54+
55+
</Step>
56+
57+
<Step title="Run the local dev server">
58+
59+
```bash
60+
npx trigger.dev@latest dev
61+
```
62+
63+
You'll get a local server that behaves like production. Your runs appear in the dashboard as you test.
64+
65+
</Step>
66+
67+
</Steps>
68+
69+
---
70+
71+
## Common patterns
72+
73+
### Webhook trigger
74+
75+
In n8n you use a **Webhook** trigger node, which registers a URL that starts the workflow.
76+
77+
In Trigger.dev, your existing route handler receives the webhook and triggers the task:
78+
79+
<CodeGroup>
80+
81+
```ts app/api/webhook/route.ts
82+
import { processWebhook } from "@/trigger/process-webhook";
83+
84+
export async function POST(request: Request) {
85+
const body = await request.json();
86+
87+
await processWebhook.trigger({
88+
event: body.event,
89+
data: body.data,
90+
});
91+
92+
return Response.json({ received: true });
93+
}
94+
```
95+
96+
```ts trigger/process-webhook.ts
97+
import { task } from "@trigger.dev/sdk";
98+
99+
export const processWebhook = task({
100+
id: "process-webhook",
101+
run: async (payload: { event: string; data: Record<string, unknown> }) => {
102+
// handle the webhook payload
103+
await handleEvent(payload.event, payload.data);
104+
},
105+
});
106+
```
107+
108+
</CodeGroup>
109+
110+
---
111+
112+
### Chaining steps (Sub-workflows)
113+
114+
In n8n you use the **Execute Sub-workflow** node to call another workflow and wait for the result.
115+
116+
In Trigger.dev you use `triggerAndWait()`:
117+
118+
<CodeGroup>
119+
120+
```ts trigger/process-order.ts
121+
import { task } from "@trigger.dev/sdk";
122+
import { sendConfirmationEmail } from "./send-confirmation-email";
123+
124+
export const processOrder = task({
125+
id: "process-order",
126+
run: async (payload: { orderId: string; email: string }) => {
127+
const result = await processPayment(payload.orderId);
128+
129+
// trigger a subtask and wait for it to complete
130+
await sendConfirmationEmail.triggerAndWait({
131+
email: payload.email,
132+
orderId: payload.orderId,
133+
amount: result.amount,
134+
});
135+
136+
return { processed: true };
137+
},
138+
});
139+
```
140+
141+
```ts trigger/send-confirmation-email.ts
142+
import { task } from "@trigger.dev/sdk";
143+
144+
export const sendConfirmationEmail = task({
145+
id: "send-confirmation-email",
146+
run: async (payload: { email: string; orderId: string; amount: number }) => {
147+
await sendEmail({
148+
to: payload.email,
149+
subject: `Order ${payload.orderId} confirmed`,
150+
body: `Your order for $${payload.amount} has been confirmed.`,
151+
});
152+
},
153+
});
154+
```
155+
156+
</CodeGroup>
157+
158+
To trigger multiple subtasks in parallel and wait for all of them (like the **Merge** node in n8n):
159+
160+
```ts trigger/process-batch.ts
161+
import { task } from "@trigger.dev/sdk";
162+
import { processItem } from "./process-item";
163+
164+
export const processBatch = task({
165+
id: "process-batch",
166+
run: async (payload: { items: { id: string }[] }) => {
167+
// fan out to subtasks, collect all results
168+
const results = await processItem.batchTriggerAndWait(
169+
payload.items.map((item) => ({ payload: { id: item.id } }))
170+
);
171+
172+
return { processed: results.runs.length };
173+
},
174+
});
175+
```
176+
177+
---
178+
179+
### Error handling
180+
181+
In n8n you use **Continue On Fail** on individual nodes and a separate **Error Workflow** for workflow-level failures.
182+
183+
In Trigger.dev:
184+
185+
- Use `try/catch` for recoverable errors at a specific step
186+
- Use the `onFailure` hook for workflow-level failure handling
187+
- Configure `retry` for automatic retries with backoff
188+
189+
```ts trigger/import-data.ts
190+
import { task } from "@trigger.dev/sdk";
191+
192+
export const importData = task({
193+
id: "import-data",
194+
// automatic retries with exponential backoff
195+
retry: {
196+
maxAttempts: 3,
197+
},
198+
// runs if this task fails after all retries
199+
onFailure: async ({ payload, error }) => {
200+
await sendAlertToSlack(`import-data failed: ${(error as Error).message}`);
201+
},
202+
run: async (payload: { source: string }) => {
203+
let records;
204+
205+
// continue on fail equivalent: catch the error and handle locally
206+
try {
207+
records = await fetchFromSource(payload.source);
208+
} catch (error) {
209+
records = await fetchFromFallback(payload.source);
210+
}
211+
212+
await saveRecords(records);
213+
},
214+
});
215+
```
216+
217+
---
218+
219+
### Waiting and delays
220+
221+
In n8n you use the **Wait** node to pause a workflow for a fixed time or until a webhook is called.
222+
223+
In Trigger.dev:
224+
225+
```ts trigger/send-followup.ts
226+
import { task, wait } from "@trigger.dev/sdk";
227+
228+
export const sendFollowup = task({
229+
id: "send-followup",
230+
run: async (payload: { userId: string; email: string }) => {
231+
await sendWelcomeEmail(payload.email);
232+
233+
// wait for a fixed duration, execution is frozen, you don't pay while waiting
234+
await wait.for({ days: 3 });
235+
236+
const hasActivated = await checkUserActivation(payload.userId);
237+
if (!hasActivated) {
238+
await sendFollowupEmail(payload.email);
239+
}
240+
},
241+
});
242+
```
243+
244+
To wait for an external event (like n8n's "On Webhook Call" resume mode), use `wait.createToken()` to generate a URL, send that URL to the external system, then pause with `wait.forToken()` until the external system POSTs to that URL to resume the run.
245+
246+
```ts trigger/approval-flow.ts
247+
import { task, wait } from "@trigger.dev/sdk";
248+
249+
export const approvalFlow = task({
250+
id: "approval-flow",
251+
run: async (payload: { requestId: string; approverEmail: string }) => {
252+
// create a token, this generates a URL the external system can POST to
253+
const token = await wait.createToken({
254+
timeout: "48h",
255+
tags: [`request-${payload.requestId}`],
256+
});
257+
258+
// send the token URL to whoever needs to resume this run
259+
await sendApprovalRequest(payload.approverEmail, payload.requestId, token.url);
260+
261+
// pause until the external system POSTs to token.url
262+
const result = await wait.forToken<{ approved: boolean }>(token).unwrap();
263+
264+
if (result.approved) {
265+
await executeApprovedAction(payload.requestId);
266+
} else {
267+
await notifyRejection(payload.requestId);
268+
}
269+
},
270+
});
271+
```
272+
273+
---
274+
275+
## Full example: customer onboarding workflow
276+
277+
Here's how a typical back office onboarding workflow translates from n8n to Trigger.dev.
278+
279+
**The n8n setup:** Webhook Trigger → HTTP Request (provision account) → HTTP Request (send welcome email) → HTTP Request (notify Slack) → Wait node (3 days) → HTTP Request (check activation) → IF node → HTTP Request (send follow-up).
280+
281+
**In Trigger.dev**, the same workflow is plain TypeScript:
282+
283+
```ts trigger/onboard-customer.ts
284+
import { task, wait } from "@trigger.dev/sdk";
285+
import { provisionAccount } from "./provision-account";
286+
import { sendWelcomeEmail } from "./send-welcome-email";
287+
288+
export const onboardCustomer = task({
289+
id: "onboard-customer",
290+
retry: {
291+
maxAttempts: 3,
292+
},
293+
run: async (payload: {
294+
customerId: string;
295+
email: string;
296+
plan: "starter" | "pro" | "enterprise";
297+
}) => {
298+
// provision their account, throws if the subtask fails
299+
await provisionAccount
300+
.triggerAndWait({
301+
customerId: payload.customerId,
302+
plan: payload.plan,
303+
})
304+
.unwrap();
305+
306+
// send welcome email
307+
await sendWelcomeEmail
308+
.triggerAndWait({
309+
customerId: payload.customerId,
310+
email: payload.email,
311+
})
312+
.unwrap();
313+
314+
// notify the team
315+
await notifySlack(`New customer: ${payload.email} on ${payload.plan}`);
316+
317+
// wait 3 days, then check if they've activated
318+
await wait.for({ days: 3 });
319+
320+
const activated = await checkActivation(payload.customerId);
321+
if (!activated) {
322+
await sendActivationNudge(payload.email);
323+
}
324+
325+
return { customerId: payload.customerId, activated };
326+
},
327+
});
328+
```
329+
330+
Trigger the workflow from your app when a new customer signs up:
331+
332+
```ts
333+
import { onboardCustomer } from "@/trigger/onboard-customer";
334+
335+
await onboardCustomer.trigger({
336+
customerId: customer.id,
337+
email: customer.email,
338+
plan: customer.plan,
339+
});
340+
```
341+
342+
Every run is visible in the Trigger.dev dashboard with full logs, retry history, and the ability to replay any run.

0 commit comments

Comments
 (0)