343 lines
12 KiB
Plaintext
343 lines
12 KiB
Plaintext
---
|
|
title: "Migrating from n8n"
|
|
description: "A practical guide for moving your n8n workflows to Trigger.dev"
|
|
sidebarTitle: "Migrating from n8n"
|
|
---
|
|
|
|
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.
|
|
|
|
Your task code runs on Trigger.dev's managed infrastructure, so there are no servers for you to provision or maintain.
|
|
|
|
## Concept map
|
|
|
|
| n8n | Trigger.dev |
|
|
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
| Workflow | [task](/tasks/overview) plus its config (queue, retry, onFailure) |
|
|
| Schedule Trigger | [schedules.task](/tasks/scheduled) |
|
|
| Webhook node | Route handler + [task.trigger()](/triggering) |
|
|
| Node | A step or library call inside `run()` |
|
|
| Execute Sub-workflow node (wait for completion) | [tasks.triggerAndWait()](/triggering#yourtask-triggerandwait) |
|
|
| Execute Sub-workflow node (execute in background) | [tasks.trigger()](/triggering) |
|
|
| Loop over N items → Execute Sub-workflow → Merge | [tasks.batchTriggerAndWait()](/tasks#yourtask-batchtriggerandwait) |
|
|
| Loop Over Items (Split in Batches) | `for` loop or `.map()` |
|
|
| IF / Switch node | `if` / `switch` statements |
|
|
| Wait node (time interval or specific time) | [wait.for()](/wait-for) or [wait.until()](/wait-until) |
|
|
| Error Trigger node / Error Workflow | [onFailure](/tasks/overview#onfailure-function) hook (both collapse into one concept in Trigger.dev) |
|
|
| Continue On Fail | `try/catch` around an individual step |
|
|
| Stop And Error | `throw new Error(...)` |
|
|
| Code node | A function or step within `run()` |
|
|
| Credentials | [Environment variable secret](/deploy-environment-variables) |
|
|
| Execution | Run (visible in the dashboard with full logs) |
|
|
| Retry on Fail (per-node setting) | [retry.maxAttempts](/tasks/overview#retry) (retries the whole `run()`, not a single step) |
|
|
| AI Agent node | Any AI SDK called inside `run()` (Vercel AI SDK, Claude SDK, OpenAI SDK, etc.) |
|
|
| Respond to Webhook node | Route handler + [task.triggerAndWait()](/triggering#yourtask-triggerandwait) returning the result as HTTP response |
|
|
|
|
---
|
|
|
|
## Setup
|
|
|
|
<Steps>
|
|
|
|
<Step title="Create an account">
|
|
|
|
Go to [Trigger.dev Cloud](https://cloud.trigger.dev), create an account, and create a project.
|
|
|
|
</Step>
|
|
|
|
<Step title="Install the CLI and initialize">
|
|
|
|
```bash
|
|
npx trigger.dev@latest init
|
|
```
|
|
|
|
This adds Trigger.dev to your project and creates a `trigger/` directory for your tasks.
|
|
|
|
</Step>
|
|
|
|
<Step title="Run the local dev server">
|
|
|
|
```bash
|
|
npx trigger.dev@latest dev
|
|
```
|
|
|
|
You'll get a local server that behaves like production. Your runs appear in the dashboard as you test.
|
|
|
|
</Step>
|
|
|
|
</Steps>
|
|
|
|
---
|
|
|
|
## Common patterns
|
|
|
|
### Webhook trigger
|
|
|
|
In n8n you use a **Webhook** trigger node, which registers a URL that starts the workflow.
|
|
|
|
In Trigger.dev, your existing route handler receives the webhook and triggers the task:
|
|
|
|
<CodeGroup>
|
|
|
|
```ts app/api/webhook/route.ts
|
|
import { processWebhook } from "@/trigger/process-webhook";
|
|
|
|
export async function POST(request: Request) {
|
|
const body = await request.json();
|
|
|
|
await processWebhook.trigger({
|
|
event: body.event,
|
|
data: body.data,
|
|
});
|
|
|
|
return Response.json({ received: true });
|
|
}
|
|
```
|
|
|
|
```ts trigger/process-webhook.ts
|
|
import { task } from "@trigger.dev/sdk";
|
|
|
|
export const processWebhook = task({
|
|
id: "process-webhook",
|
|
run: async (payload: { event: string; data: Record<string, unknown> }) => {
|
|
// handle the webhook payload
|
|
await handleEvent(payload.event, payload.data);
|
|
},
|
|
});
|
|
```
|
|
|
|
</CodeGroup>
|
|
|
|
---
|
|
|
|
### Chaining steps (Sub-workflows)
|
|
|
|
In n8n you use the **Execute Sub-workflow** node to call another workflow and wait for the result.
|
|
|
|
In Trigger.dev you use `triggerAndWait()`:
|
|
|
|
<CodeGroup>
|
|
|
|
```ts trigger/process-order.ts
|
|
import { task } from "@trigger.dev/sdk";
|
|
import { sendConfirmationEmail } from "./send-confirmation-email";
|
|
|
|
export const processOrder = task({
|
|
id: "process-order",
|
|
run: async (payload: { orderId: string; email: string }) => {
|
|
const result = await processPayment(payload.orderId);
|
|
|
|
// trigger a subtask and wait for it to complete
|
|
await sendConfirmationEmail.triggerAndWait({
|
|
email: payload.email,
|
|
orderId: payload.orderId,
|
|
amount: result.amount,
|
|
});
|
|
|
|
return { processed: true };
|
|
},
|
|
});
|
|
```
|
|
|
|
```ts trigger/send-confirmation-email.ts
|
|
import { task } from "@trigger.dev/sdk";
|
|
|
|
export const sendConfirmationEmail = task({
|
|
id: "send-confirmation-email",
|
|
run: async (payload: { email: string; orderId: string; amount: number }) => {
|
|
await sendEmail({
|
|
to: payload.email,
|
|
subject: `Order ${payload.orderId} confirmed`,
|
|
body: `Your order for $${payload.amount} has been confirmed.`,
|
|
});
|
|
},
|
|
});
|
|
```
|
|
|
|
</CodeGroup>
|
|
|
|
To trigger multiple subtasks in parallel and wait for all of them (like the **Merge** node in n8n):
|
|
|
|
```ts trigger/process-batch.ts
|
|
import { task } from "@trigger.dev/sdk";
|
|
import { processItem } from "./process-item";
|
|
|
|
export const processBatch = task({
|
|
id: "process-batch",
|
|
run: async (payload: { items: { id: string }[] }) => {
|
|
// fan out to subtasks, collect all results
|
|
const results = await processItem.batchTriggerAndWait(
|
|
payload.items.map((item) => ({ payload: { id: item.id } }))
|
|
);
|
|
|
|
return { processed: results.runs.length };
|
|
},
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
### Error handling
|
|
|
|
In n8n you use **Continue On Fail** on individual nodes and a separate **Error Workflow** for workflow-level failures.
|
|
|
|
In Trigger.dev:
|
|
|
|
- Use `try/catch` for recoverable errors at a specific step
|
|
- Use the `onFailure` hook for workflow-level failure handling
|
|
- Configure `retry` for automatic retries with backoff
|
|
|
|
```ts trigger/import-data.ts
|
|
import { task } from "@trigger.dev/sdk";
|
|
|
|
export const importData = task({
|
|
id: "import-data",
|
|
// automatic retries with exponential backoff
|
|
retry: {
|
|
maxAttempts: 3,
|
|
},
|
|
// runs if this task fails after all retries
|
|
onFailure: async ({ payload, error }) => {
|
|
await sendAlertToSlack(`import-data failed: ${(error as Error).message}`);
|
|
},
|
|
run: async (payload: { source: string }) => {
|
|
let records;
|
|
|
|
// continue on fail equivalent: catch the error and handle locally
|
|
try {
|
|
records = await fetchFromSource(payload.source);
|
|
} catch (error) {
|
|
records = await fetchFromFallback(payload.source);
|
|
}
|
|
|
|
await saveRecords(records);
|
|
},
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
### Waiting and delays
|
|
|
|
In n8n you use the **Wait** node to pause a workflow for a fixed time or until a webhook is called.
|
|
|
|
In Trigger.dev:
|
|
|
|
```ts trigger/send-followup.ts
|
|
import { task, wait } from "@trigger.dev/sdk";
|
|
|
|
export const sendFollowup = task({
|
|
id: "send-followup",
|
|
run: async (payload: { userId: string; email: string }) => {
|
|
await sendWelcomeEmail(payload.email);
|
|
|
|
// wait for a fixed duration, execution is frozen, you don't pay while waiting
|
|
await wait.for({ days: 3 });
|
|
|
|
const hasActivated = await checkUserActivation(payload.userId);
|
|
if (!hasActivated) {
|
|
await sendFollowupEmail(payload.email);
|
|
}
|
|
},
|
|
});
|
|
```
|
|
|
|
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.
|
|
|
|
```ts trigger/approval-flow.ts
|
|
import { task, wait } from "@trigger.dev/sdk";
|
|
|
|
export const approvalFlow = task({
|
|
id: "approval-flow",
|
|
run: async (payload: { requestId: string; approverEmail: string }) => {
|
|
// create a token, this generates a URL the external system can POST to
|
|
const token = await wait.createToken({
|
|
timeout: "48h",
|
|
tags: [`request-${payload.requestId}`],
|
|
});
|
|
|
|
// send the token URL to whoever needs to resume this run
|
|
await sendApprovalRequest(payload.approverEmail, payload.requestId, token.url);
|
|
|
|
// pause until the external system POSTs to token.url
|
|
const result = await wait.forToken<{ approved: boolean }>(token).unwrap();
|
|
|
|
if (result.approved) {
|
|
await executeApprovedAction(payload.requestId);
|
|
} else {
|
|
await notifyRejection(payload.requestId);
|
|
}
|
|
},
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Full example: customer onboarding workflow
|
|
|
|
Here's how a typical back office onboarding workflow translates from n8n to Trigger.dev.
|
|
|
|
**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).
|
|
|
|
**In Trigger.dev**, the same workflow is plain TypeScript:
|
|
|
|
```ts trigger/onboard-customer.ts
|
|
import { task, wait } from "@trigger.dev/sdk";
|
|
import { provisionAccount } from "./provision-account";
|
|
import { sendWelcomeEmail } from "./send-welcome-email";
|
|
|
|
export const onboardCustomer = task({
|
|
id: "onboard-customer",
|
|
retry: {
|
|
maxAttempts: 3,
|
|
},
|
|
run: async (payload: {
|
|
customerId: string;
|
|
email: string;
|
|
plan: "starter" | "pro" | "enterprise";
|
|
}) => {
|
|
// provision their account, throws if the subtask fails
|
|
await provisionAccount
|
|
.triggerAndWait({
|
|
customerId: payload.customerId,
|
|
plan: payload.plan,
|
|
})
|
|
.unwrap();
|
|
|
|
// send welcome email
|
|
await sendWelcomeEmail
|
|
.triggerAndWait({
|
|
customerId: payload.customerId,
|
|
email: payload.email,
|
|
})
|
|
.unwrap();
|
|
|
|
// notify the team
|
|
await notifySlack(`New customer: ${payload.email} on ${payload.plan}`);
|
|
|
|
// wait 3 days, then check if they've activated
|
|
await wait.for({ days: 3 });
|
|
|
|
const activated = await checkActivation(payload.customerId);
|
|
if (!activated) {
|
|
await sendActivationNudge(payload.email);
|
|
}
|
|
|
|
return { customerId: payload.customerId, activated };
|
|
},
|
|
});
|
|
```
|
|
|
|
Trigger the workflow from your app when a new customer signs up:
|
|
|
|
```ts
|
|
import { onboardCustomer } from "@/trigger/onboard-customer";
|
|
|
|
await onboardCustomer.trigger({
|
|
customerId: customer.id,
|
|
email: customer.email,
|
|
plan: customer.plan,
|
|
});
|
|
```
|
|
|
|
Every run is visible in the Trigger.dev dashboard with full logs, retry history, and the ability to replay any run.
|