yaml
title: “Trigger.dev V2: TypeScript Durable Execution Without Workflow DSLs” description: “How Trigger.dev pivoted from event triggers to durable tasks, exposing retry semantics, state persistence, and execution isolation for agent loops.” pubDate: 2026-06-06T00:15:09.092492Z category: dev-tools heroImage: “https://images.unsplash.com/photo-1568716353609-12ddc5c67f04?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w4Njk2ODR8MHwxfHNlYXJjaHwxfHxUcmlnZ2VyLmRldiUyMFYyJTNBJTIwV2hhdCUyMGElMjBUZW1wb3JhbCUyMEFsdGVybmF0aXZlJTIwZm9yJTIwVHlwZVNjcmlwdCUyMFJldmVhbHMlMjBBYm91dCUyMER1cmFibGUlMjBFeGVjdXRpb24lMjBQbHVtYmluZyUyMHNvZnR3YXJlJTIwaW5mcmFzdHJ1Y3R1cmUlMjBkYXNoYm9hcmR8ZW58MXwwfHx8MTc4MDY4OTcwOHww&ixlib=rb-4.1.0&q=80&w=1080” sourceUrl: “https://trigger.dev” sourceName: “trigger.dev” tags: [“agentic-ai”, “orchestration”, “infrastructure”] featured: false
Trigger.dev launched in February 2023 as a developer-first Zapier alternative. By October 2023, the team had pivoted to V2 with a new positioning: a Temporal alternative for TypeScript. That shift exposes a critical infrastructure gap. Developers building multi-step agent workflows need durable execution primitives, not just webhook glue.
The V1-to-V2 pivot reveals what happens when you try to build long-running AI tasks on top of event-driven infrastructure. Event triggers work for fire-and-forget actions. They break down when you need guaranteed execution across retries, state snapshots between steps, and resumable functions that survive pod restarts.
What Changed Between V1 and V2
V1 treated tasks as event handlers. You defined a trigger (webhook, schedule, event), wrote a handler function, and Trigger.dev routed the event. This works for simple automations: send a Slack message when a GitHub issue opens, resize an image on S3 upload.
V2 reframed tasks as durable execution units. The runtime now guarantees:
- Automatic retries with exponential backoff for transient failures
- State persistence between function invocations
- Resumable execution after infrastructure crashes
- Observability hooks for every step in a multi-stage workflow
The code surface looks similar (you still export a task object), but the execution model changed completely. V1 tasks were stateless functions. V2 tasks are state machines with checkpointing.
Execution Isolation and State Snapshots
Trigger.dev runs tasks in isolated Node.js or Bun processes. When a task calls an external API or waits on a long-running operation, the runtime serializes the execution state to durable storage (Postgres or S3, depending on deployment mode). If the worker crashes, the scheduler replays the task from the last checkpoint.
This is different from Temporal’s approach. Temporal uses event sourcing: every state transition emits an event, and the workflow replays all events to reconstruct state. Trigger.dev snapshots the JavaScript heap at await boundaries. The trade-off:
| Approach | Replay Cost | State Size | Language Lock-In |
|---|---|---|---|
| Event sourcing (Temporal) | High (replay all events) | Small (event log) | Low (polyglot workers) |
| Heap snapshots (Trigger.dev) | Low (restore from snapshot) | Large (serialized heap) | High (TypeScript/JS only) |
For TypeScript-native teams, heap snapshots eliminate the need to learn workflow DSLs or think about event replay semantics. You write async functions. The runtime handles durability.
Retry Semantics for Agent Loops
Agent workflows fail in predictable ways: rate limits, transient API errors, model timeouts. Trigger.dev’s retry logic is tunable per task:
export const researchAgent = task({
id: "research-agent",
retry: {
maxAttempts: 5,
factor: 2,
minTimeout: 1000,
maxTimeout: 60000,
},
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) {
const result = await executeTool(call);
messages.push({ role: "tool", content: result });
}
}
},
});
The retry config applies to the entire task. If generateText throws a rate limit error, Trigger.dev waits (exponential backoff), then re-invokes the function from the last checkpoint. The messages array persists across retries because it’s part of the serialized heap.
This is critical for agent loops. A 10-step research task might call 30 external APIs. Without durable execution, a single transient failure at step 8 forces you to restart from step 1 or write custom checkpointing logic.
Observability and Debugging
Trigger.dev exposes a real-time trace view for every task execution. You see:
- Step-by-step execution timeline with wall-clock duration
- Tool call payloads and responses for each LLM interaction
- Retry attempts with failure reasons
- State snapshots at each checkpoint
The observability model assumes you’re debugging non-deterministic workflows. Agent tasks don’t follow a fixed DAG. The LLM decides which tools to call and in what order. Traditional workflow visualizations (Airflow DAGs, Prefect flow charts) don’t help. You need a trace that shows what actually happened, not what you planned.
Deployment Shape and Scaling
Trigger.dev offers two deployment modes:
- Managed cloud: Tasks run on Trigger.dev’s infrastructure. You push code, they handle workers, queues, and state storage.
- Self-hosted: You run the Trigger.dev runtime in your own Kubernetes cluster or VPC. State goes to your Postgres instance.
The managed cloud uses Firecracker VMs for task isolation. Each task gets a dedicated microVM with a configurable memory limit (512MB to 8GB). The scheduler auto-scales workers based on queue depth.
Self-hosted deployments use Docker containers instead of Firecracker. You lose the sub-second cold start times but gain control over the execution environment (custom base images, private npm registries, VPC-only network access).
Concurrency and Queue Control
Trigger.dev lets you define concurrency limits per task or per queue:
export const imageProcessor = task({
id: "process-image",
queue: {
name: "image-processing",
concurrencyLimit: 10,
},
run: async ({ imageUrl }: { imageUrl: string }) => {
// Heavy CPU work
},
});
This prevents thundering herd problems. If 1,000 images arrive at once, only 10 tasks run concurrently. The rest wait in the queue. The scheduler respects priority (you can assign weights to tasks) and fairness (round-robin across queues).
For agent workflows, concurrency control matters when you’re calling rate-limited APIs. You can set a global concurrency limit for all tasks that call the OpenAI API, ensuring you stay under your org’s token-per-minute quota.
When TypeScript Heap Snapshots Break Down
Heap snapshots assume your task state is serializable. If your task holds open file handles, WebSocket connections, or database cursors, the snapshot will fail. Trigger.dev throws a runtime error if it detects non-serializable objects in the heap.
This is a hard constraint. You can’t checkpoint mid-stream. If you’re processing a 10GB file, you either:
- Load the entire file into memory (hits memory limits)
- Stream it in chunks and checkpoint between chunks (requires manual state management)
- Use a different execution model (Temporal’s event sourcing handles streams better)
For most agent tasks, this isn’t a problem. LLM calls are request-response. Tool executions are discrete. But if you’re building a task that processes real-time data streams or holds long-lived connections, Trigger.dev’s snapshot model won’t fit.
Comparing Trigger.dev and Temporal
| Dimension | Trigger.dev | Temporal |
|---|---|---|
| Language support | TypeScript, JavaScript | Go, Java, Python, TypeScript, PHP |
| State model | Heap snapshots | Event sourcing |
| Execution guarantees | At-least-once | Exactly-once (with idempotency) |
| Cold start time | <1s (Firecracker) | 2-5s (gRPC worker pool) |
| Observability | Real-time trace UI | Event history + external APM |
| Learning curve | Low (async functions) | High (workflow DSL, activities) |
| Self-hosting complexity | Medium (Docker + Postgres) | High (Cassandra/MySQL + Elasticsearch) |
Temporal gives you stronger guarantees (exactly-once execution, deterministic replay) at the cost of operational complexity. Trigger.dev trades some guarantees for developer ergonomics. You write normal TypeScript. The runtime handles retries and state.
Failure Modes to Watch
-
Heap snapshot size limits: If your task accumulates too much state (large arrays, deep object graphs), the snapshot will exceed storage limits. You’ll see serialization errors.
-
Non-deterministic code: If your task uses
Math.random()orDate.now()without seeding, replays from checkpoints will produce different results. Trigger.dev doesn’t enforce determinism like Temporal does. -
External side effects: If your task writes to a database, then retries, you might write duplicate records. Trigger.dev doesn’t provide idempotency tokens. You need to handle deduplication in your task logic.
-
Queue backpressure: If tasks enqueue faster than workers can process them, queue depth grows unbounded. The managed cloud auto-scales, but self-hosted deployments need manual worker scaling.
Technical Verdict
Use Trigger.dev when:
- You’re building agent workflows in TypeScript and want durable execution without learning workflow DSLs
- Your tasks are request-response (API calls, LLM invocations, database queries) rather than long-lived streams
- You need fast iteration cycles and don’t want to manage Temporal’s operational complexity
- You’re okay with at-least-once execution semantics and will handle idempotency in your task code
Avoid Trigger.dev when:
- You need exactly-once execution guarantees (financial transactions, payment processing)
- Your tasks process large data streams or hold long-lived connections
- You need polyglot worker support (Python, Go, Java)
- You’re already running Temporal and have invested in its operational model
Trigger.dev’s V2 pivot exposes a real need: TypeScript developers want durable execution primitives without the ceremony of traditional workflow engines. The heap snapshot model works well for agent loops where state is discrete and side effects are idempotent. It breaks down when you need streaming, exactly-once semantics, or polyglot workers.