mech.app
Automation

Trigger.dev V2: How a TypeScript Workflow Engine Competes with Temporal Without Go or gRPC

Architecture comparison of Trigger.dev's durable execution model, retry primitives, and deployment shape against Temporal's Go runtime.

Source: trigger.dev
Trigger.dev V2: How a TypeScript Workflow Engine Competes with Temporal Without Go or gRPC

Trigger.dev started as a Zapier alternative (745 points, February 2023), then pivoted to a Temporal alternative for TypeScript developers (172 points, October 2023). The shift matters because it signals a second-generation approach to workflow orchestration: code-first durable execution without adopting Temporal’s Go runtime, event sourcing model, or gRPC protocol.

This article examines the plumbing. How does Trigger.dev implement durable execution, retries, and long-running tasks in TypeScript? What are the tradeoffs in developer experience, deployment complexity, and operational guarantees compared to Temporal?

Architecture: Durable Execution Without Event Sourcing

Temporal uses event sourcing. Every workflow decision is logged as an immutable event, replayed on resume, and stored in a history database. Trigger.dev uses a simpler model: checkpoint-based state persistence.

Trigger.dev’s execution model:

  • Tasks are TypeScript functions decorated with task().
  • State is serialized at explicit checkpoints (before network calls, retries, or waits).
  • The runtime stores checkpoints in Postgres, not an append-only event log.
  • On resume, the runtime rehydrates state from the last checkpoint and continues.

Why this matters:

  • No replay semantics. You can use non-deterministic code (random numbers, Date.now()) without breaking resumption.
  • Simpler mental model. You write async functions, not workflow definitions with strict determinism rules.
  • Lower storage overhead. Checkpoints replace full event histories.

Tradeoff:

  • Less auditability. You lose the immutable event log that Temporal provides for debugging and compliance.
  • Replay-based time travel is harder. Temporal can replay workflows from any point in history; Trigger.dev resumes from the last checkpoint.

Retry and Failure Handling

Temporal workflows are deterministic and replay-safe. Retries happen at the activity level, with exponential backoff configured in code. Trigger.dev tasks are non-deterministic, so retries happen at the task level with configurable strategies.

Trigger.dev retry primitives:

export const processOrder = task({
  id: "process-order",
  retry: {
    maxAttempts: 5,
    factor: 2,
    minTimeout: 1000,
    maxTimeout: 60000,
    randomize: true
  },
  run: async ({ orderId }: { orderId: string }) => {
    const order = await db.orders.findUnique({ where: { id: orderId } });
    await stripe.charges.create({ amount: order.total, currency: "usd" });
    await sendEmail({ to: order.email, template: "receipt" });
    return { status: "completed" };
  }
});

Retry behavior:

  • Retries are automatic on unhandled exceptions.
  • Backoff is configurable per task, not per step.
  • Idempotency is your responsibility. Trigger.dev does not deduplicate retries.

Temporal comparison:

FeatureTrigger.devTemporal
Retry scopeTask-levelActivity-level
IdempotencyManual (use external keys)Built-in (activity IDs)
Backoff configPer-task decoratorPer-activity options
DeterminismNot requiredRequired for workflows
Replay safetyCheckpoint-basedEvent-sourced

Long-Running Tasks and Timeouts

Temporal workflows can run for years. Activities have configurable timeouts (start-to-close, schedule-to-start, heartbeat). Trigger.dev tasks can run indefinitely, but timeout behavior is simpler.

Trigger.dev timeout model:

  • No global workflow timeout. Tasks run until completion or failure.
  • Heartbeat timeouts are not built-in. You implement keepalive logic manually.
  • Long-running tasks checkpoint periodically to avoid losing progress.

Example: multi-hour video processing

export const processVideo = task({
  id: "process-video",
  run: async ({ videoUrl }: { videoUrl: string }) => {
    const chunks = await splitVideo(videoUrl);
    
    for (const chunk of chunks) {
      // Checkpoint before each expensive operation
      await wait.for({ seconds: 1 }); // Forces checkpoint
      const transcoded = await transcodeChunk(chunk);
      await uploadChunk(transcoded);
    }
    
    return { status: "done", chunks: chunks.length };
  }
});

Key difference:

  • Temporal’s heartbeat mechanism detects stuck activities and retries them.
  • Trigger.dev relies on task-level retries and external monitoring.

State Management and Observability

Temporal stores workflow state in a dedicated history database (Cassandra or Postgres). Trigger.dev stores task state in your application’s Postgres database.

Trigger.dev state schema:

  • tasks table: task definitions, retry config, concurrency limits.
  • runs table: execution state, checkpoints, logs, output.
  • events table: task lifecycle events (started, completed, failed).

Observability:

  • Real-time task monitoring via dashboard.
  • Structured logs with trace IDs.
  • No distributed tracing out of the box (you add OpenTelemetry).

Temporal comparison:

  • Temporal provides built-in tracing, metrics, and workflow history UI.
  • Trigger.dev requires you to wire up observability yourself.

Deployment Shape

Temporal requires:

  • A Go-based server cluster (frontend, history, matching, worker services).
  • gRPC for client-server communication.
  • A separate worker process per language SDK.

Trigger.dev requires:

  • A Node.js runtime (Bun or Node 18+).
  • A Postgres database.
  • Optional: a separate worker process for background tasks.

Self-hosted deployment:

# docker-compose.yml
services:
  trigger:
    image: trigger.dev/engine:latest
    environment:
      DATABASE_URL: postgres://user:pass@db:5432/trigger
      SECRET_KEY: your-secret-key
    ports:
      - "3000:3000"
  
  db:
    image: postgres:15
    environment:
      POSTGRES_DB: trigger
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass

Managed cloud:

  • Trigger.dev Cloud handles infrastructure, scaling, and observability.
  • You deploy tasks as TypeScript functions; the platform runs them.

Concurrency and Queues

Temporal uses task queues and worker pools. Trigger.dev uses concurrency limits and priority queues.

Trigger.dev concurrency model:

export const sendEmail = task({
  id: "send-email",
  queue: {
    name: "email",
    concurrencyLimit: 10 // Max 10 concurrent executions
  },
  run: async ({ to, subject, body }) => {
    await smtp.send({ to, subject, body });
  }
});

Queue behavior:

  • Tasks in the same queue respect the concurrency limit.
  • Priority is configurable per task invocation.
  • No built-in rate limiting (you implement it with delays or external tools).

Security Boundaries

Temporal workflows run in isolated worker processes. Trigger.dev tasks run in the same Node.js process by default.

Isolation model:

  • Tasks share the same runtime and memory space.
  • No sandboxing between tasks.
  • Secrets are injected via environment variables or a secrets manager.

Risk:

  • A malicious or buggy task can crash the entire worker process.
  • No built-in multi-tenancy. You isolate tenants at the deployment level.

Failure Modes

Trigger.dev failure scenarios:

  1. Database unavailable: Tasks cannot checkpoint or resume. Executions are lost.
  2. Worker crash: In-flight tasks restart from the last checkpoint.
  3. Non-idempotent retry: Duplicate charges, emails, or API calls.
  4. Checkpoint bloat: Large task state increases storage and resume latency.

Mitigation:

  • Use external idempotency keys (Stripe idempotency tokens, database unique constraints).
  • Monitor checkpoint size and refactor tasks that accumulate too much state.
  • Run multiple worker processes for redundancy.

When to Use Trigger.dev

Good fit:

  • You write TypeScript and want durable execution without learning Temporal’s determinism rules.
  • You need long-running tasks (hours, not days) with retries and observability.
  • You want to self-host on a single Postgres database without a Go cluster.
  • You are building agent workflows that checkpoint between tool calls.

Bad fit:

  • You need years-long workflows with full event history for compliance.
  • You require strict determinism and replay-based debugging.
  • You need built-in multi-tenancy and sandboxing.
  • You already run Temporal and want to avoid rewriting workflows.

Technical Verdict

Trigger.dev trades Temporal’s event sourcing and determinism guarantees for a simpler TypeScript-native developer experience. The checkpoint-based model works well for agent workflows, media processing, and background jobs that run for hours, not years. The lack of built-in idempotency and sandboxing means you must handle those concerns yourself.

Use Trigger.dev when you want durable execution without the operational overhead of a Go-based orchestrator. Avoid it when you need immutable audit logs, replay-based debugging, or workflows that span months.