Skip to content

Latest commit

 

History

History
340 lines (254 loc) · 11.8 KB

File metadata and controls

340 lines (254 loc) · 11.8 KB
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 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

Setup

Go to Trigger.dev Cloud, create an account, and create a project.

npx trigger.dev@latest init

This adds Trigger.dev to your project and creates a trigger/ directory for your tasks.

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:

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);
  },
});
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():

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 };
  },
});

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

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);
    }
  },
});

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:

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.