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:
- Persists the execution state before the failed step.
- Schedules a retry using the backoff policy.
- Replays the task from the last successful checkpoint.
- 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:
| Aspect | Temporal | Trigger.dev V2 |
|---|---|---|
| State source | Event log replay | Snapshot at await boundaries |
| Recovery speed | Slower (full replay) | Faster (jump to last checkpoint) |
| Audit trail | Complete event history | Step-level execution graph |
| Determinism requirement | Strict (no random, Date.now) | Relaxed (snapshots capture state) |
| Storage overhead | Log grows with retries | Fixed 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:
- Task-level defaults: Apply to all steps unless overridden.
- Step-level overrides: Wrap specific operations with custom retry logic.
- 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:
- Managed cloud: Tasks run in their infrastructure. You push code, they handle workers.
- Self-hosted workers: You run the worker process. State still persists to your Postgres.
- 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:
| Requirement | Trigger.dev | Temporal |
|---|---|---|
| TypeScript-first DX | Strong | Weak (SDK exists but not idiomatic) |
| Managed hosting | Yes (cloud) | No (self-host or Temporal Cloud) |
| Event sourcing | No (snapshots) | Yes (full replay) |
| Complex DAGs | Limited | Strong |
| Operational overhead | Low | High |
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.