mech.app
Automation

Trigger.dev V2: What a Temporal Alternative for TypeScript Reveals About Durable Execution Plumbing

How Trigger.dev pivoted from event triggers to durable execution, exposing the infrastructure gap for long-running TypeScript jobs.

Source: trigger.dev
Trigger.dev V2: What a Temporal Alternative for TypeScript Reveals About Durable Execution Plumbing

Trigger.dev started as a Zapier alternative in February 2023 (745 HN points). By October 2023, the team shipped V2 and repositioned as a Temporal alternative for TypeScript (172 points). That pivot tells you something useful about what developers actually need when they build agent-like background tasks: not event routing, but durable execution with replay semantics and state persistence that doesn’t require a PhD in distributed systems.

The shift exposes a real infrastructure gap. Temporal gives you workflow orchestration with strong guarantees, but it’s Go-based, requires a cluster, and forces you into a specific mental model. Trigger.dev bets that TypeScript developers want long-running jobs with retries, queues, and observability without leaving their existing stack or deploying a separate worker fleet.

What Durable Execution Means Here

Durable execution is not just “retry on failure.” It’s the ability to pause, resume, and replay a task across process restarts, network failures, and timeouts without losing state or duplicating side effects.

Trigger.dev handles this by:

  • Checkpointing state automatically at await boundaries in your TypeScript code
  • Replaying from the last checkpoint when a task crashes or times out
  • Persisting execution history so you can inspect what happened at each step
  • Guaranteeing at-least-once delivery with idempotency keys for external API calls

You write normal async TypeScript. The runtime intercepts await calls, snapshots state, and stores it in a backing database (Postgres or SQLite). When a task resumes, it skips already-completed steps and picks up where it left off.

This is the same pattern Temporal uses, but Trigger.dev compiles it into a TypeScript-native runtime instead of requiring a separate worker process and gRPC protocol.

Retry and Failure Semantics

Trigger.dev gives you three retry strategies out of the box:

StrategyBehaviorUse Case
Exponential backoff1s, 2s, 4s, 8s, up to maxTransient API failures
Fixed delaySame interval every timeRate-limited endpoints
Manual interventionTask pauses, waits for human approvalCompliance workflows, budget checks

Dead-letter queues are optional. You can configure a task to move to a separate queue after N failures, or you can let it fail permanently and trigger an alert.

The runtime tracks failure reasons in structured logs. If a task fails because of a network timeout, you see the exact HTTP status code and response body. If it fails because of a code exception, you get the stack trace and the state snapshot from the last checkpoint.

State Persistence Without Manual Checkpoints

The key difference from Temporal is that you don’t manually call workflow.sleep() or activity.execute(). You just write:

export const processOrder = task({
  id: "process-order",
  run: async ({ orderId }: { orderId: string }) => {
    // Checkpoint 1: Fetch order
    const order = await db.orders.findUnique({ where: { id: orderId } });
    
    // Checkpoint 2: Charge payment
    const charge = await stripe.charges.create({
      amount: order.total,
      currency: "usd",
      customer: order.customerId,
    });
    
    // Checkpoint 3: Send confirmation
    await sendEmail({
      to: order.email,
      subject: "Order confirmed",
      body: `Your order ${orderId} is confirmed.`,
    });
    
    return { orderId, chargeId: charge.id };
  },
});

If the Stripe API call times out, the task restarts from checkpoint 2. The database query doesn’t re-run. The email doesn’t send twice (assuming sendEmail is idempotent or wrapped in a deduplication layer).

This works because Trigger.dev instruments the TypeScript runtime. Every async function call becomes a potential checkpoint. The runtime serializes the call arguments, stores them, and marks the step as complete when it resolves.

Deployment Shape

Trigger.dev runs in two modes:

  1. Managed cloud: You push code to their platform, they handle workers, scaling, and persistence.
  2. Self-hosted: You run the Trigger.dev server and workers in your own infrastructure (Docker, Kubernetes, or bare metal).

In both cases, you define tasks in your codebase and deploy them via CLI:

npx trigger.dev@latest deploy

The CLI bundles your tasks, uploads them to the Trigger.dev API, and registers them in the task registry. Workers poll the registry for new tasks and execute them in isolated sandboxes.

Workers are stateless. They pull task definitions from the registry, load the execution state from the database, run the next step, and write the updated state back. If a worker crashes, another worker picks up the task and resumes from the last checkpoint.

This is different from Temporal, where you deploy workflow code separately from the Temporal cluster and workers connect via gRPC. Trigger.dev collapses the deployment surface: one CLI command, one API endpoint, one worker fleet.

Observability and Debugging

The Trigger.dev dashboard shows:

  • Task execution timeline: Every checkpoint, retry, and failure with timestamps
  • State snapshots: The exact arguments and return values at each step
  • Logs and traces: Structured logs from your code, plus OpenTelemetry spans
  • Concurrency and queue depth: How many tasks are running, waiting, or retrying

You can click into a failed task, see the exception, and replay it from the last checkpoint without redeploying code. This is critical for debugging agent workflows where failures happen deep in a multi-step chain.

The observability layer is built on top of the execution history. Every checkpoint is a database row. Every retry is a new row with a link to the previous attempt. You can query this data directly via SQL if you need custom dashboards or alerting.

Failure Modes

Trigger.dev assumes:

  • Your database is durable: If Postgres goes down, tasks pause until it comes back.
  • Your code is deterministic: Non-deterministic code (random numbers, timestamps) will break replay semantics.
  • External APIs are idempotent: If you call a non-idempotent API twice, you’ll get duplicate side effects.

The runtime doesn’t protect you from these issues. It’s your job to:

  • Use idempotency keys for external API calls
  • Avoid reading Date.now() or Math.random() inside tasks
  • Ensure your database has replication and backups

If you violate these assumptions, tasks will replay incorrectly or produce inconsistent results.

When to Use Trigger.dev vs. Temporal

FactorTrigger.devTemporal
LanguageTypeScript onlyGo, Java, Python, TypeScript
DeploymentManaged or self-hosted, single binaryRequires cluster (Cassandra or MySQL)
Learning curveLow (write async functions)High (workflow vs. activity model)
ObservabilityBuilt-in dashboardRequires separate tooling (Temporal Web)
ScalabilityHorizontal (add workers)Horizontal (add workers + cluster nodes)
Failure recoveryAutomatic replay from checkpointsAutomatic replay from event history

Use Trigger.dev if you:

  • Work primarily in TypeScript and want minimal deployment overhead
  • Need durable execution for background jobs, not full workflow orchestration
  • Want built-in observability without setting up Temporal Web or Grafana

Avoid Trigger.dev if you:

  • Need multi-language support (Temporal supports Go, Java, Python)
  • Already run a Temporal cluster and have workflows in production
  • Need advanced features like child workflows, signals, or queries

Technical Verdict

Trigger.dev solves the “I just want long-running TypeScript jobs with retries” problem without forcing you into Temporal’s operational complexity. The automatic checkpointing is clever, the observability is good, and the deployment story is simple.

The trade-off is language lock-in and reliance on deterministic code. If you’re building agent workflows in TypeScript and don’t need Temporal’s full feature set, Trigger.dev is a solid choice. If you need multi-language support or already have Temporal expertise, stick with Temporal.

The V1-to-V2 pivot is a useful reminder that developers don’t want event routing. They want durable execution with good defaults and minimal plumbing.