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 retry semantics, state persistence, and TypeScript ergonomics for long-runnin...

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

Trigger.dev started as a “Zapier alternative for developers” in February 2023 (745 HN points). By October, the team shipped V2 and repositioned as a “Temporal alternative for TypeScript devs” (172 points). That pivot exposes a real architectural gap: developers building agentic systems don’t need event routing. They need durable execution with resumable state, retry semantics, and TypeScript-native APIs that don’t force them into YAML or Java.

The shift reveals what background job infrastructure actually looks like when you strip away the workflow DSL and focus on the execution boundary.

What Changed Between V1 and V2

V1 was event-driven. You defined triggers (webhooks, schedules, database events) and actions. The model assumed short-lived handlers that completed in seconds.

V2 is execution-driven. You define tasks that can run for hours, pause, retry, and resume. The model assumes long-running processes with explicit state checkpoints.

Key architectural differences:

  • State persistence: V1 stored event payloads. V2 stores execution snapshots at await boundaries.
  • Retry semantics: V1 used simple exponential backoff. V2 lets you define per-step retry policies with custom backoff curves.
  • Observability: V1 logged events. V2 traces execution graphs with step-level timing and error attribution.
  • Deployment model: V1 required a persistent server. V2 runs tasks in ephemeral workers with managed state externalization.

The TypeScript SDK wraps this in a task primitive that looks like a normal async function but gets instrumented for durability.

Execution Model: How Tasks Become Durable

Trigger.dev tasks are TypeScript functions wrapped in a task() call. The runtime intercepts await points and persists execution state to Postgres (self-hosted) or their managed cloud storage.

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

export const processDocument = task({
  id: "process-document",
  retry: {
    maxAttempts: 3,
    factor: 2,
    minTimeout: 1000,
  },
  run: async (payload: { url: string }) => {
    // Step 1: Download (checkpoint here)
    const file = await downloadFile(payload.url);
    
    // Step 2: Extract text (checkpoint here)
    const text = await extractText(file);
    
    // Step 3: Analyze (checkpoint here)
    const analysis = await analyzeContent(text);
    
    return { analysis, wordCount: text.split(" ").length };
  },
});

When downloadFile throws a network error, the runtime:

  1. Persists the execution state before the failed step.
  2. Schedules a retry using the backoff policy.
  3. Replays the task from the last successful checkpoint.
  4. Skips already-completed steps (idempotency via step IDs).

This is not event sourcing like Temporal. Trigger.dev snapshots state at await boundaries instead of replaying a full event log. The trade-off: faster recovery, but less audit trail granularity.

State Management and Replay Boundaries

Temporal rebuilds state by replaying the entire workflow history. Trigger.dev persists serialized execution context at each await point.

Replay strategy comparison:

AspectTemporalTrigger.dev V2
State sourceEvent log replaySnapshot at await boundaries
Recovery speedSlower (full replay)Faster (jump to last checkpoint)
Audit trailComplete event historyStep-level execution graph
Determinism requirementStrict (no random, Date.now)Relaxed (snapshots capture state)
Storage overheadLog grows with retriesFixed per checkpoint

Trigger.dev’s approach means you can use Date.now() or Math.random() inside a task without breaking replay. The snapshot includes those values. Temporal requires deterministic execution because it rebuilds state from scratch.

The downside: you lose the ability to time-travel through execution history. Temporal lets you inspect every decision point. Trigger.dev shows you the checkpoints, not the path between them.

Retry Semantics and Failure Modes

Trigger.dev exposes retry configuration at three levels:

  1. Task-level defaults: Apply to all steps unless overridden.
  2. Step-level overrides: Wrap specific operations with custom retry logic.
  3. Manual intervention: Pause execution and wait for external input.
export const fragileApiCall = task({
  id: "fragile-api",
  retry: {
    maxAttempts: 5,
    factor: 3,
    minTimeout: 2000,
    maxTimeout: 60000,
  },
  run: async (payload) => {
    // This step has different retry rules
    const criticalData = await retry.fetch(
      "https://api.example.com/critical",
      {
        maxAttempts: 10,
        factor: 1.5,
      }
    );
    
    // This step fails fast
    const metadata = await retry.fetch(
      "https://api.example.com/metadata",
      {
        maxAttempts: 1,
      }
    );
    
    return { criticalData, metadata };
  },
});

Failure handling:

  • Transient errors (network timeouts, rate limits): Automatic retry with exponential backoff.
  • Permanent errors (404, auth failures): Task moves to failed state immediately.
  • Partial failures: Completed steps don’t re-run. Only failed steps retry.

Dead-letter queues are manual. If a task exhausts retries, it enters a failed state. You query failed tasks via the API and decide whether to retry, modify, or discard.

TypeScript SDK Boundary and Type Safety

Trigger.dev doesn’t serialize closures. Task payloads must be JSON-serializable. The SDK uses Zod schemas under the hood to validate inputs and outputs at runtime.

import { z } from "zod";

const PayloadSchema = z.object({
  userId: z.string(),
  action: z.enum(["create", "update", "delete"]),
  data: z.record(z.unknown()),
});

export const userAction = task({
  id: "user-action",
  run: async (payload: z.infer<typeof PayloadSchema>) => {
    // TypeScript knows payload.action is "create" | "update" | "delete"
    // Runtime validates against schema before execution starts
  },
});

The execution boundary is the network call to the Trigger.dev API. Your application code triggers tasks via HTTP. The SDK handles serialization, authentication, and idempotency keys.

Type safety guarantees:

  • Compile-time: TypeScript infers payload and return types from Zod schemas.
  • Runtime: Zod validates payloads before task execution starts.
  • Cross-version: Schema changes break loudly instead of silently corrupting state.

No code generation required. The SDK uses TypeScript’s type inference to keep schemas and types in sync.

Deployment Model and Observability

Trigger.dev supports three deployment shapes:

  1. Managed cloud: Tasks run in their infrastructure. You push code, they handle workers.
  2. Self-hosted workers: You run the worker process. State still persists to your Postgres.
  3. Hybrid: Workers in your VPC, control plane in their cloud.

The worker process polls for tasks, executes them, and reports checkpoints back to the control plane. This is different from Temporal’s worker-as-sidecar model. Trigger.dev workers are stateless. All execution state lives in Postgres or their managed storage.

Observability stack:

  • Execution graph: Visual tree of steps with timing and retry counts.
  • Real-time logs: Streamed from workers to the dashboard.
  • Trace propagation: OpenTelemetry-compatible spans for each step.
  • Metrics export: Prometheus-compatible endpoint for task duration, retry rate, failure rate.

The dashboard shows you which step failed, how many times it retried, and the exact error message. You can replay failed tasks from the UI without touching code.

When to Use Trigger.dev vs. Temporal

Trigger.dev fits when:

  • Your team writes TypeScript and wants to avoid learning a new DSL.
  • You need long-running tasks (hours to days) with automatic retries.
  • You want managed infrastructure without operating a Temporal cluster.
  • Your workflows are linear or tree-shaped (not complex DAGs).

Temporal fits when:

  • You need strict determinism and full event sourcing.
  • Your workflows require complex branching, parallel execution, or saga patterns.
  • You already run Java or Go services and want workflow orchestration in the same stack.
  • You need to audit every decision point in a workflow for compliance.

Trade-off table:

RequirementTrigger.devTemporal
TypeScript-first DXStrongWeak (SDK exists but not idiomatic)
Managed hostingYes (cloud)No (self-host or Temporal Cloud)
Event sourcingNo (snapshots)Yes (full replay)
Complex DAGsLimitedStrong
Operational overheadLowHigh

Trigger.dev is a background job queue with durability. Temporal is a workflow engine with state machines. If your “workflow” is actually a sequence of API calls with retries, Trigger.dev is simpler. If you’re orchestrating microservices with compensating transactions, Temporal gives you more control.

Technical Verdict

Use Trigger.dev when you need durable execution for TypeScript tasks without operating a distributed system. The snapshot-based replay model trades audit granularity for faster recovery and simpler mental models. The managed cloud option removes infrastructure toil.

Avoid it when you need strict determinism, complex workflow patterns, or deep integration with non-TypeScript services. The execution model assumes linear or tree-shaped task graphs. If your workflows require dynamic parallelism or saga compensation, Temporal’s event sourcing gives you better primitives.

For agentic systems that chain LLM calls, tool invocations, and external API requests, Trigger.dev’s retry semantics and checkpoint-based recovery handle the common case: transient failures in long-running processes. The TypeScript SDK keeps agent code readable without forcing you into YAML or visual workflow builders.