mech.app
Automation

Trigger.dev V2: Building a TypeScript Workflow Engine Without Temporal's Event-Sourcing Model

How Trigger.dev handles retries, state persistence, and long-running tasks in TypeScript without adopting Temporal's Go-based architecture.

Source: trigger.dev
Trigger.dev V2: Building a TypeScript Workflow Engine Without Temporal's Event-Sourcing Model

Trigger.dev started as a Zapier alternative in February 2023 (745 HN points), then pivoted seven months later to a Temporal alternative for TypeScript developers (172 points). The V2 rewrite signals a fundamental architectural shift: from webhook-driven integrations to durable execution primitives. The question is how they handle workflow state, retries, and long-running tasks without adopting Temporal’s event-sourcing model or requiring a separate Go-based orchestrator.

The Pivot: From Integrations to Execution Primitives

V1 focused on pre-built integrations and trigger definitions. V2 exposes lower-level primitives:

  • Task definitions with automatic retry logic
  • Durable delays that survive process restarts
  • Concurrency controls and queue management
  • Observability hooks for tracing and monitoring

The shift reflects what early users actually needed: not another integration marketplace, but a way to write reliable background jobs in the same language as their application code.

State Persistence Without Event Sourcing

Temporal persists every workflow decision as an immutable event log. Trigger.dev takes a checkpoint-based approach:

  1. Execution checkpoints: The engine snapshots task state at specific boundaries (before retries, after delays, at fan-out points).
  2. Idempotency keys: Each task invocation gets a unique identifier. Retries replay from the last successful checkpoint rather than re-executing the entire workflow.
  3. Database-backed state: Task state lives in Postgres or compatible stores. No separate event history service.

This trades Temporal’s full auditability for simpler infrastructure. You lose the ability to replay arbitrary historical states, but you gain a deployment model that fits existing TypeScript stacks.

TypeScript-Native Workflow Primitives

The V2 API exposes workflows as TypeScript functions decorated with task metadata:

import { task } from "@trigger.dev/sdk/v3";

export const processOrder = task({
  id: "process-order",
  retry: {
    maxAttempts: 3,
    factor: 2,
    minTimeout: 1000,
  },
  run: async (payload: { orderId: string }) => {
    // Step 1: Charge payment
    const charge = await stripe.charges.create({
      amount: payload.amount,
      currency: "usd",
    });

    // Step 2: Wait for fulfillment (durable delay)
    await wait.for({ seconds: 3600 });

    // Step 3: Send confirmation
    await sendEmail({
      to: payload.email,
      subject: "Order shipped",
    });

    return { orderId: payload.orderId, status: "complete" };
  },
});

Key primitives:

  • wait.for(): Durable delays that persist across restarts. The engine checkpoints before the wait, then reschedules the continuation.
  • retry config: Declarative backoff policies. The engine tracks attempt counts in the checkpoint.
  • run function: Standard async TypeScript. No special DSL or code generation.

Scheduling and Fan-Out Patterns

Trigger.dev handles three common orchestration patterns:

Cron Schedules

export const dailyReport = task({
  id: "daily-report",
  schedule: {
    cron: "0 9 * * *",
    timezone: "America/New_York",
  },
  run: async () => {
    // Durable cron without external scheduler
  },
});

The engine stores schedule metadata in the task definition. No separate cron daemon required.

Batch Fan-Out

export const batchProcess = task({
  id: "batch-process",
  run: async (payload: { items: string[] }) => {
    const results = await Promise.all(
      payload.items.map((item) =>
        processItem.triggerAndWait({ item })
      )
    );
    return results;
  },
});

triggerAndWait() spawns child tasks and blocks until all complete. The parent task checkpoints after each child finishes, so partial failures don’t lose progress.

Human-in-the-Loop

export const approvalFlow = task({
  id: "approval-flow",
  run: async (payload: { documentId: string }) => {
    await sendApprovalRequest(payload.documentId);

    // Wait up to 7 days for approval
    const approved = await wait.forEvent({
      event: "document.approved",
      timeout: { days: 7 },
    });

    if (!approved) {
      throw new Error("Approval timeout");
    }

    return { status: "approved" };
  },
});

The wait.forEvent() primitive suspends the task until an external event arrives or the timeout expires. The engine checkpoints the suspended state and resumes when the event fires.

Versioning and Hot-Reloading

Temporal requires explicit workflow versioning because the orchestrator replays history against the current code. Trigger.dev avoids this by checkpointing execution state rather than decision logs.

When you deploy new task code:

  1. In-flight tasks continue running the old version. The checkpoint includes a reference to the deployed code version.
  2. New tasks start with the latest code.
  3. No replay conflicts because the engine doesn’t re-execute completed steps.

This simplifies deployments but creates a different problem: you can’t easily patch bugs in running workflows. If a task is suspended for days, it will resume with the old buggy code.

Tradeoffs: Language-Native vs. Polyglot Orchestrators

DimensionTrigger.dev (TypeScript-native)Temporal (Polyglot)
DeploymentSingle Node.js processSeparate orchestrator cluster + workers
State modelCheckpoint-basedEvent-sourced history
Language supportTypeScript onlyGo, Java, Python, PHP, .NET
VersioningCode version per checkpointExplicit workflow versioning
AuditabilityTask execution logsFull decision history replay
Infra complexityPostgres + task queueTemporal server + Cassandra/Postgres
Failure recoveryRetry from last checkpointReplay from event log

Trigger.dev wins on simplicity and TypeScript ergonomics. Temporal wins on auditability and cross-language workflows.

Observability and Failure Modes

Trigger.dev exposes real-time task traces through a web dashboard:

  • Execution timeline: Shows each checkpoint, retry, and delay.
  • Logs and errors: Captured per task invocation.
  • Queue depth: Monitors backlog and concurrency limits.

Common failure modes:

  1. Checkpoint corruption: If the database write fails mid-checkpoint, the task may retry from an inconsistent state. Mitigation: transactional checkpoint writes.
  2. Clock skew: Durable delays rely on wall-clock time. If the system clock jumps, scheduled tasks may fire early or late.
  3. Memory leaks in long tasks: Unlike Temporal’s deterministic replay, Trigger.dev tasks run as normal async functions. A memory leak in a multi-hour task will eventually crash the worker.
  4. Versioning drift: If a task is suspended for weeks, it resumes with outdated code. No automatic migration path.

Security Boundaries

Trigger.dev runs user code in the same process as the orchestration engine. This creates two risks:

  • Resource exhaustion: A runaway task can starve other tasks. Mitigation: per-task memory and CPU limits (requires containerization).
  • Credential leakage: Tasks share the same runtime environment. Secrets must be scoped per task, not per process.

Temporal isolates workflow logic (deterministic, sandboxed) from activity logic (side effects, unrestricted). Trigger.dev collapses this boundary, trading isolation for simplicity.

Deployment Shape

Trigger.dev offers two deployment models:

Managed Cloud

  • Hosted task queue and execution workers
  • Automatic scaling based on queue depth
  • Built-in observability dashboard

Self-Hosted

  • Docker container with Postgres backend
  • Requires manual scaling and monitoring
  • Suitable for air-gapped or compliance-sensitive environments

Both models use the same TypeScript SDK. The main difference is who operates the infrastructure.

Technical Verdict

Use Trigger.dev when:

  • Your team writes TypeScript and wants workflow primitives without learning a new orchestration DSL.
  • You need durable retries and delays but don’t require full event-sourcing auditability.
  • You want to deploy background jobs alongside your Next.js or Express app without running a separate orchestrator cluster.
  • Your workflows are hours to days long, not weeks to months.

Avoid Trigger.dev when:

  • You need cross-language workflows (Python ML models calling Java services).
  • You require full audit trails for compliance (financial transactions, healthcare).
  • Your workflows run for weeks and must survive code changes mid-execution.
  • You need deterministic replay for debugging complex state machines.

Trigger.dev is a pragmatic choice for TypeScript-native teams building AI agents, media pipelines, or human-in-the-loop workflows. It trades Temporal’s rigor for deployment simplicity. If you can tolerate checkpoint-based recovery instead of event-sourced replay, the architecture fits cleanly into existing Node.js stacks.