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-native workflow trade-offs.

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

Trigger.dev launched in February 2023 as an “open source Zapier alternative” and earned 745 points on Hacker News. Eight months later, the team shipped V2 and repositioned as a “Temporal alternative for TypeScript.” The pivot tells a concrete story about what developers building agents and long-running automations actually need: not event routing, but durable task execution with retry semantics, state persistence, and observability baked in.

V2’s checkpoint-based state model differs fundamentally from Temporal’s event sourcing. This affects retry semantics, replay capability, and the determinism requirements you must satisfy in workflow code. For teams building agentic systems or data pipelines in TypeScript, the trade-offs between Temporal’s polyglot event sourcing model and a TypeScript-native runtime matter at the code level, deployment boundary, and failure recovery layer.

What Changed Between V1 and V2

V1 focused on event-driven triggers. GitHub webhooks, Slack events, Stripe notifications. You wrote handlers that responded to external events, similar to Zapier’s trigger-action model. The execution model assumed short-lived functions that completed within seconds or minutes.

V2 pivoted to durable execution. The founder’s admission in the Show HN post was direct: “We gathered a lot of feedback from early users and realized that what developers actually wanted” was not event routing but reliable background job execution. Tasks can run for hours or days, survive server restarts, and retry individual steps without re-executing the entire workflow. The mental model shifted from “respond to this event” to “execute this long-running job reliably.”

Key architectural changes:

  • State persistence: Task state is checkpointed to a database (Postgres by default). If a worker crashes mid-execution, the task resumes from the last checkpoint.
  • Retry boundaries: You define retry policies per task, not per function call. Failed steps retry automatically with exponential backoff.
  • Execution guarantees: Tasks execute at least once. Idempotency is your responsibility, but the runtime guarantees delivery.
  • Observability primitives: Built-in tracing, logs, and execution history. No need to wire up OpenTelemetry manually.

The V2 SDK structure looks like this:

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

export const processLargeDataset = task({
  id: "process-dataset",
  run: async (payload: { datasetId: string }) => {
    // Step 1: Fetch data (checkpointed)
    const data = await fetchDataset(payload.datasetId);
    
    // Step 2: Transform (checkpointed)
    const transformed = await transformData(data);
    
    // Step 3: Upload results (checkpointed)
    await uploadResults(transformed);
    
    return { recordsProcessed: data.length };
  },
  retry: {
    maxAttempts: 3,
    factor: 2,
    minTimeout: 1000,
    maxTimeout: 10000,
  },
});

Each await creates an implicit checkpoint. If the worker dies after transformData completes, the task resumes at uploadResults without re-fetching or re-transforming.

State Persistence vs. Event Sourcing

Temporal uses event sourcing. Every workflow decision is recorded as an immutable event. Replay those events, and you reconstruct the workflow state. This enables deterministic replay and time travel debugging, but it requires strict determinism in your workflow code. No random numbers, no direct API calls inside workflow functions. You must use activities (side-effect containers) for anything non-deterministic.

Trigger.dev checkpoints state at explicit boundaries. The runtime serializes task state to Postgres after each await. This is simpler to reason about but less flexible for complex branching logic. The dashboard does not expose historical checkpoint versions, so you resume from the most recent state snapshot rather than replaying from an arbitrary point in execution history.

DimensionTemporalTrigger.dev V2
State modelEvent sourcingCheckpoint snapshots
Determinism requirementStrict (workflow code must be pure)Relaxed (checkpoints handle side effects)
Replay capabilityFull history replayResume from last checkpoint
DebuggingTime travel to any eventInspect checkpoint state

For TypeScript teams, Trigger.dev’s relaxed determinism model means you can call APIs directly in task code without wrapping them in activities. The trade-off: you lose full replay capability and must trust checkpoint consistency.

Retry Semantics and Failure Modes

Trigger.dev’s retry logic operates at the task level. You configure maxAttempts, factor, and timeout bounds when defining the task. If a step fails, the entire task retries from the last checkpoint.

This differs from Temporal’s activity retries, which operate at the activity (side-effect) level. A Temporal workflow can retry a single activity 10 times while the workflow itself continues. Trigger.dev retries the entire task, which can be wasteful if only one step fails repeatedly.

Failure modes to consider:

  • Non-idempotent steps: If a step has side effects (e.g., charging a credit card), retrying the entire task can cause duplicate actions. You must implement idempotency keys manually.
  • Timeout boundaries: Trigger.dev enforces a maximum task duration (configurable, default 1 hour). Long-running tasks must be split into subtasks or use the wait primitive to pause execution.

Example of handling idempotency:

export const chargeCustomer = task({
  id: "charge-customer",
  run: async (payload: { customerId: string, amount: number, orderId: string }) => {
    // Use stable identifier from payload for idempotency
    const charge = await stripe.charges.create({
      customer: payload.customerId,
      amount: payload.amount,
      idempotency_key: `order-${payload.orderId}`,
    });
    
    await db.charges.insert({ 
      chargeId: charge.id, 
      customerId: payload.customerId,
      orderId: payload.orderId 
    });
    
    return { chargeId: charge.id };
  },
});

If the task fails after creating the Stripe charge but before updating the database, retrying will not create a duplicate charge because the idempotency key is derived from the stable orderId. Trigger.dev does not enforce this pattern; it is your responsibility to implement.

Developer Ergonomics: Local Dev and Deployment

Trigger.dev provides a CLI for local development. You run npx trigger.dev dev to start a local worker that connects to the Trigger.dev cloud or your self-hosted instance. The worker polls for tasks and executes them locally, with full access to your local environment (database, file system, environment variables).

This is simpler than Temporal’s local setup, which requires running a Temporal server (via Docker Compose) and configuring workers to connect to it. Trigger.dev’s managed runtime abstracts the orchestration layer, so you only manage worker code.

Deployment options:

  • Managed runtime: Trigger.dev hosts the orchestration layer and workers. You push code via the CLI, and tasks run in their infrastructure.
  • Self-hosted workers: You run workers in your own infrastructure (Kubernetes, ECS, Lambda) and connect to Trigger.dev’s managed orchestration layer.
  • Fully self-hosted: You run both the orchestration layer (Postgres + API server) and workers. This requires managing database migrations, scaling workers, and monitoring task queues.

The managed runtime is the fastest path to production but locks you into Trigger.dev’s infrastructure. Self-hosted workers give you control over execution environment (VPC, secrets, compliance) while offloading orchestration complexity. Fully self-hosted is only necessary for air-gapped environments or strict data residency requirements.

Observability and Debugging

Trigger.dev’s dashboard shows task execution history, logs, and retry attempts. Each task run has a trace view that displays step-by-step execution, including checkpoint timestamps and error stack traces.

Key observability primitives:

  • Structured logs: Tasks emit logs that are indexed by task ID, run ID, and timestamp. You can filter by log level (info, warn, error) and search by message content.
  • Execution traces: Each task run generates a trace with spans for each checkpointed step. Spans include duration, input/output payloads, and error details.
  • Metrics: Task-level metrics (success rate, p95 latency, retry count) are aggregated in the dashboard. You can set up alerts for failure thresholds.

The dashboard does not expose raw event logs (unlike Temporal’s event history), so debugging requires inspecting checkpoint state and logs. This simplifies the mental model but reduces debugging flexibility for complex failure scenarios. If a task fails due to a transient error (e.g., rate limit), you can manually retry from the dashboard without re-triggering the entire workflow.

When to Use Trigger.dev vs. Temporal

Trigger.dev fits TypeScript teams that need durable background jobs without the operational overhead of running a Temporal cluster. It works well for:

  • Agent orchestration: Multi-step AI workflows with tool calls, retries, and human-in-the-loop approvals.
  • Data pipelines: ETL jobs that process large datasets over hours or days, with checkpointed progress.
  • Scheduled tasks: Cron jobs that must survive server restarts and retry on failure.

Consider Temporal if you need:

  • Polyglot workflows: Temporal supports Go, Java, Python, and TypeScript. Trigger.dev is TypeScript only.
  • Complex branching logic: Temporal’s event sourcing model handles arbitrary workflow graphs better than checkpoint-based state.
  • Full replay capability: Temporal can replay workflows from any point in history. Trigger.dev only resumes from the last checkpoint.
  • Mature self-hosted deployment: Temporal’s self-hosted option is more established for environments without internet access.

Technical Verdict

Trigger.dev V2 is a pragmatic choice for TypeScript teams building durable background jobs. The checkpoint-based state model is easier to reason about than event sourcing, and the managed runtime eliminates the operational burden of running a Temporal cluster. The trade-off is reduced flexibility: you cannot replay arbitrary workflow states, and retry semantics are coarser-grained.

Use Trigger.dev if your team prioritizes time-to-production and TypeScript-native development over polyglot orchestration and deterministic replay. The V1 to V2 pivot, documented in the Show HN discussions, reveals a market lesson: developers building agents and automations need durable execution more than event routing. The infrastructure requirements (retry semantics, state persistence, observability) are non-negotiable, and abstracting them into a managed runtime is a valid product strategy for teams that value developer velocity over architectural flexibility.

Primary Sources: