|
| 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