| 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.
| n8n | Trigger.dev |
|---|---|
| Workflow | task plus its config (queue, retry, onFailure) |
| Schedule Trigger | schedules.task |
| Webhook node | Route handler + task.trigger() |
| Node | A step or library call inside run() |
| Execute Sub-workflow node (wait for completion) | tasks.triggerAndWait() |
| Execute Sub-workflow node (execute in background) | tasks.trigger() |
| Loop over N items → Execute Sub-workflow → Merge | tasks.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() or wait.until() |
| Error Trigger node / Error Workflow | onFailure 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 |
| Execution | Run (visible in the dashboard with full logs) |
| Retry on Fail (per-node setting) | retry.maxAttempts (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() returning the result as HTTP response |
Go to Trigger.dev Cloud, create an account, and create a project.
npx trigger.dev@latest initThis adds Trigger.dev to your project and creates a trigger/ directory for your tasks.
npx trigger.dev@latest devYou'll get a local server that behaves like production. Your runs appear in the dashboard as you test.
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:
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 });
}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);
},
});In n8n you use the Execute Sub-workflow node to call another workflow and wait for the result.
In Trigger.dev you use triggerAndWait():
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 };
},
});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.`,
});
},
});To trigger multiple subtasks in parallel and wait for all of them (like the Merge node in n8n):
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 };
},
});In n8n you use Continue On Fail on individual nodes and a separate Error Workflow for workflow-level failures.
In Trigger.dev:
- Use
try/catchfor recoverable errors at a specific step - Use the
onFailurehook for workflow-level failure handling - Configure
retryfor automatic retries with backoff
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);
},
});In n8n you use the Wait node to pause a workflow for a fixed time or until a webhook is called.
In Trigger.dev:
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.
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);
}
},
});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:
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:
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.