From 0213d98dcc62b0807542873e963a8a9f631d4849 Mon Sep 17 00:00:00 2001 From: isshaddad Date: Thu, 26 Mar 2026 19:25:38 -0400 Subject: [PATCH 1/2] n8n concept map and common patterns --- docs/docs.json | 2 +- docs/migration-n8n.mdx | 271 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 docs/migration-n8n.mdx diff --git a/docs/docs.json b/docs/docs.json index 779b5d53fb5..b2ae678febf 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -470,7 +470,7 @@ }, { "group": "Migration guides", - "pages": ["migration-mergent"] + "pages": ["migration-mergent", "migration-n8n"] }, { "group": "Community packages", diff --git a/docs/migration-n8n.mdx b/docs/migration-n8n.mdx new file mode 100644 index 00000000000..e8167238826 --- /dev/null +++ b/docs/migration-n8n.mdx @@ -0,0 +1,271 @@ +--- +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. + +## 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 + + + + + +Go to [Trigger.dev Cloud](https://cloud.trigger.dev), create an account, and create a project. + + + + + +```bash +npx trigger.dev@latest init +``` + +This adds Trigger.dev to your project and creates a `trigger/` directory for your tasks. + + + + + +```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. + + + + + +--- + +## 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: + + + +```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 }) => { + // handle the webhook payload + await handleEvent(payload.event, payload.data); + }, +}); +``` + +```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 }); +} +``` + + + +--- + +### 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()`: + + + +```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.`, + }); + }, +}); +``` + + + +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); + } + }, +}); +``` + +--- From 0b1d5a396627982afef0cdbae0242e3c55018b4e Mon Sep 17 00:00:00 2001 From: isshaddad Date: Fri, 27 Mar 2026 11:25:50 -0400 Subject: [PATCH 2/2] customer onboarding workflow example --- docs/migration-n8n.mdx | 71 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/docs/migration-n8n.mdx b/docs/migration-n8n.mdx index e8167238826..cd793f58c99 100644 --- a/docs/migration-n8n.mdx +++ b/docs/migration-n8n.mdx @@ -247,7 +247,7 @@ 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 + // create a token, this generates a URL the external system can POST to const token = await wait.createToken({ timeout: "48h", tags: [`request-${payload.requestId}`], @@ -269,3 +269,72 @@ export const approvalFlow = task({ ``` --- + +## 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.