mech.app
Automation

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

How Trigger.dev pivoted from webhooks to durable execution, exposing retry semantics, state persistence, and TypeScript-native orchestration patterns.

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

Trigger.dev started as a “developer-first Zapier alternative” and pivoted hard. The V2 announcement repositioned it as a “Temporal alternative for TypeScript devs,” scoring 172 points on Hacker News after the original V1 Show HN hit 745. That shift from event-driven webhooks to durable execution tells you something about what developers building agent workflows actually need: not another webhook router, but a runtime that survives crashes, retries intelligently, and persists state without forcing you into Go or a separate workflow DSL.

The infrastructure gap is real. Zapier handles simple trigger-action chains. Temporal handles complex sagas but demands operational overhead and a learning curve that TypeScript teams resist. Trigger.dev V2 targets the middle: long-running tasks (hours or days), retry semantics that don’t lose context, and state persistence that feels native to your existing codebase.

What Changed Between V1 and V2

V1 was webhook-centric. You defined triggers (GitHub push, Stripe payment) and actions (send Slack message, update database). The execution model was stateless: fire a function, wait for it to complete, log the result. If the function crashed, you got a retry with exponential backoff, but no durable state between attempts.

V2 introduced tasks with durable execution. A task can run for hours, checkpoint its state, survive infrastructure failures, and resume exactly where it left off. The runtime serializes execution context to persistent storage, so a crashed worker can pick up mid-loop without recomputing earlier steps.

Key differences:

  • State persistence: V1 retried functions from scratch. V2 checkpoints state at await boundaries.
  • Execution model: V1 was event-driven (webhook in, response out). V2 is workflow-driven (task runs until complete, regardless of duration).
  • Retry semantics: V1 retried the entire function. V2 retries from the last successful checkpoint.

Durable Execution Without a Workflow DSL

Temporal requires you to write workflows in a restricted subset of your language. No direct database calls, no non-deterministic operations, no side effects outside the workflow context. You learn a new mental model: activities (side effects) vs. workflows (orchestration).

Trigger.dev V2 lets you write normal TypeScript. You mark a function as a task, and the runtime handles durability. Here’s what that looks like:

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

export const processLargeDataset = task({
  id: "process-dataset",
  run: async (payload: { datasetId: string }) => {
    const dataset = await db.datasets.findUnique({ 
      where: { id: payload.datasetId } 
    });
    
    for (const chunk of dataset.chunks) {
      // Checkpoint happens here automatically
      const result = await processChunk(chunk);
      await db.results.create({ data: result });
    }
    
    return { processed: dataset.chunks.length };
  }
});

Every await is a potential checkpoint. If the worker crashes after processing chunk 3 of 10, the runtime resumes at chunk 4. The state (loop index, variables, call stack) is serialized to storage between awaits.

The trick: Trigger.dev instruments your code at the transpilation layer. It wraps async calls, tracks execution flow, and persists state to a backing store (Postgres or Redis, depending on deployment). When a task resumes, it replays the execution log up to the last checkpoint, then continues forward.

Retry and Failure Semantics

Trigger.dev V2 exposes three retry strategies:

  1. Automatic retries: Transient failures (network timeout, rate limit) trigger exponential backoff. Configurable max attempts and backoff multiplier.
  2. Manual retries: You can throw a RetryError with a custom delay. Useful for API rate limits where you know the reset time.
  3. Idempotency keys: Tasks deduplicate by payload hash. If you trigger the same task twice with identical input, the second call returns the cached result.

Failure modes:

  • Transient failure: Retry with backoff. State persists across attempts.
  • Permanent failure: Task moves to failed state after max retries. You can configure dead-letter queues or alerting webhooks.
  • Timeout: Tasks have a configurable max duration. If exceeded, the task is killed and marked as timed out.

Compare this to Temporal’s saga pattern, where you explicitly define compensating transactions for each step. Trigger.dev assumes your code is idempotent or you handle rollback manually. Less ceremony, more responsibility on the developer.

State Checkpoint and Resume Mechanism

The runtime serializes execution state at every await. Here’s what gets persisted:

  • Call stack: Function frames, local variables, loop indices.
  • Async context: Promises in flight, their resolution state.
  • External state: API responses, database query results (if you opt in).

When a task resumes, the runtime:

  1. Loads the execution log from storage.
  2. Replays the log up to the last checkpoint (skipping actual I/O, using cached results).
  3. Continues execution from the next await.

This works because TypeScript compiles to JavaScript, which has a well-defined event loop. The runtime hooks into the event loop, intercepts async operations, and serializes state before yielding control.

Limitations:

  • Non-deterministic code breaks replay: If you call Math.random() or Date.now() directly, replay will produce different results. Trigger.dev provides deterministic wrappers (ctx.random(), ctx.now()).
  • Large state blows up storage: If you load a 10GB file into memory, checkpointing fails. You need to stream or paginate.
  • External side effects aren’t rolled back: If you send an email in step 3 and the task fails at step 5, the email doesn’t get unsent. You handle idempotency.

Long-Running Tasks in a Serverless Context

Serverless platforms (Lambda, Vercel Functions) have hard timeouts (15 minutes for Lambda, 5 minutes for Vercel). Trigger.dev runs tasks on its own infrastructure, not your serverless provider. You deploy task definitions to Trigger.dev’s runtime, which schedules them on long-lived workers.

Architecture:

  • Control plane: Manages task definitions, schedules, and execution logs. Runs on Trigger.dev’s cloud or your self-hosted instance.
  • Worker pool: Executes tasks. Workers are long-lived containers (not Lambda functions). They pull tasks from a queue, execute them, and checkpoint state.
  • Storage layer: Postgres for execution logs and state snapshots. Redis for task queues and locks.

For tasks that run for hours or days:

  • Workers checkpoint state every N seconds (configurable).
  • If a worker dies, the control plane reschedules the task on a healthy worker.
  • The new worker loads the latest checkpoint and resumes.

This is different from Temporal’s approach, where workflows run in a separate cluster and communicate with workers via gRPC. Trigger.dev collapses the control plane and worker into a single runtime, reducing operational complexity but limiting horizontal scalability.

Trade-Offs: Trigger.dev vs. Temporal vs. Zapier

DimensionTrigger.dev V2TemporalZapier
Execution modelDurable tasks, TypeScript-nativeWorkflows + activities, language-agnosticEvent-driven, stateless
State persistenceAutomatic at await boundariesExplicit workflow stateNone (retries from scratch)
Retry semanticsExponential backoff, idempotency keysConfigurable per activityFixed backoff, limited retries
Operational overheadManaged service or self-hosted single binaryRequires Temporal cluster (Cassandra, Elasticsearch)Fully managed, no ops
Long-running tasksHours to days, checkpointedUnlimited duration, event-sourcedMax 30 seconds per action
Developer experienceWrite normal TypeScriptLearn workflow DSL, avoid non-determinismVisual builder, limited code
ObservabilityBuilt-in dashboard, logs, tracesTemporal Web UI, requires setupBasic logs, limited debugging

Trigger.dev sits between Zapier’s simplicity and Temporal’s power. You get durable execution without learning a new paradigm, but you sacrifice Temporal’s battle-tested saga patterns and horizontal scalability.

Concrete Use Case: Multi-Step AI Agent

An AI research agent needs to:

  1. Search the web for a topic.
  2. Browse top 10 results.
  3. Analyze content with an LLM.
  4. Generate a summary.

Each step can fail (rate limits, API timeouts, model errors). The agent might run for 20 minutes. Here’s how Trigger.dev V2 handles it:

export const researchAgent = task({
  id: "research-agent",
  run: async ({ topic }: { topic: string }) => {
    const messages: CoreMessage[] = [
      { role: "user", content: `Research: ${topic}` }
    ];
    
    for (let i = 0; i < 10; i++) {
      const { text, toolCalls, steps } = await generateText({
        model: anthropic("claude-opus-4-20250514"),
        system: "You are a research assistant with web access.",
        messages,
        tools: { search, browse, analyze },
        maxSteps: 5,
      });
      
      if (!toolCalls.length) {
        return { summary: text, stepsUsed: steps.length };
      }
      
      for (const call of toolCalls) {
        // Checkpoint happens here
        const result = await executeTool(call);
        messages.push({ role: "tool", content: result });
      }
    }
  }
});

If the LLM call fails at iteration 3, the task retries from iteration 3, not iteration 0. The messages array is checkpointed, so the agent doesn’t lose conversation context.

If the task runs for 20 minutes, the worker checkpoints state every 30 seconds. If the worker crashes at minute 15, a new worker picks up at the last checkpoint (minute 14.5) and continues.

Likely Failure Modes

  1. Non-deterministic code in replay: If you use Date.now() instead of ctx.now(), replay produces different timestamps. The task might behave differently on retry.
  2. Large state blowup: If you accumulate a 100MB array in memory, checkpointing slows down or fails. You need to paginate or stream.
  3. External side effects without idempotency: If you charge a credit card in step 3 and the task retries from step 2, you charge twice. You need idempotency keys or external deduplication.
  4. Worker pool exhaustion: If you trigger 10,000 tasks simultaneously, the worker pool might saturate. You need concurrency limits or autoscaling.
  5. Storage layer bottleneck: High checkpoint frequency (every second) can overwhelm Postgres. You need to tune checkpoint intervals or use Redis for hot state.

Security Boundaries

Trigger.dev runs tasks in isolated containers, but tasks within the same project share a worker pool. If you run untrusted code, you need to:

  • Sandbox execution: Use a separate project or worker pool for untrusted tasks.
  • Limit resource usage: Set memory and CPU limits per task.
  • Validate inputs: Sanitize payloads to prevent injection attacks.

The control plane authenticates API calls with API keys. You can scope keys to specific projects or tasks. Logs and execution traces are encrypted at rest and in transit.

Observability and Debugging

Trigger.dev provides a web dashboard with:

  • Real-time task status: Running, queued, failed, completed.
  • Execution logs: Stdout, stderr, structured logs.
  • Traces: Step-by-step execution timeline, showing awaits and checkpoints.
  • Metrics: Task duration, retry count, failure rate.

You can filter by task ID, payload, or time range. The dashboard shows the exact line of code where a task failed, along with the stack trace and local variables.

For debugging, you can:

  • Replay a failed task: Trigger a retry with the same payload.
  • Inspect checkpoints: Download the serialized state and examine it locally.
  • Tail logs: Stream logs in real time as a task runs.

Deployment Shape

Trigger.dev offers two deployment modes:

  1. Managed cloud: You push task definitions to Trigger.dev’s cloud. They handle infrastructure, scaling, and monitoring. You pay per task execution.
  2. Self-hosted: You run the Trigger.dev runtime on your own infrastructure (Kubernetes, Docker Compose). You manage scaling and storage.

For self-hosted:

  • Control plane: Single Node.js process, backed by Postgres.
  • Worker pool: Horizontally scalable containers, pull tasks from Redis queue.
  • Storage: Postgres for execution logs, Redis for queues and locks.

You can run the control plane and workers on the same machine for small workloads, or scale them independently for high throughput.

Technical Verdict

Use Trigger.dev V2 when:

  • You need durable execution for tasks that run longer than serverless timeouts (15+ minutes).
  • You want to write normal TypeScript without learning a workflow DSL.
  • You’re building agent workflows with retries, state persistence, and observability.
  • You prefer managed infrastructure over operating a Temporal cluster.

Avoid Trigger.dev V2 when:

  • You need battle-tested saga patterns with compensating transactions (use Temporal).
  • You’re running tasks that scale to thousands of concurrent executions (Temporal handles this better).
  • You need multi-language support (Temporal supports Go, Java, Python, etc.).
  • You want zero operational overhead and can live with 30-second timeouts (use Zapier or n8n).

Trigger.dev V2 is a pragmatic middle ground. It gives you durable execution without the ceremony of Temporal, but you trade off some scalability and maturity. For TypeScript teams building agent workflows, it’s a solid choice if you’re willing to handle idempotency and resource limits yourself.