yaml
title: “Trigger.dev V2: Durable Execution Plumbing for TypeScript Without the JVM” description: “How Trigger.dev pivoted from webhook automation to Temporal-style task durability, exposing retry semantics, state persistence, and execution guarantees.” pubDate: 2026-06-06T08:05:10.548593Z 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” (745 HN points). By October 2023, the team shipped V2 as a “Temporal alternative for TypeScript” (172 points). That pivot exposes a real infrastructure gap: developers building long-running workflows and AI agents need durable execution guarantees without adopting Temporal’s JVM complexity or learning Go.
The shift from webhook-triggered automation to durable task execution reveals what matters when orchestrating multi-step processes that span hours or days. State persistence, retry semantics, execution isolation, and observability become first-class concerns. Here’s what the plumbing looks like.
What Changed Between V1 and V2
V1 focused on event-driven automation. You connected webhooks, APIs, and scheduled triggers to TypeScript functions. The platform handled delivery, but tasks lived in a request-response model. If your function crashed mid-execution, you lost state.
V2 rebuilt the execution model around durability:
- State persistence: Task state survives process crashes and restarts
- Automatic retries: Failed steps replay from checkpoints, not from scratch
- Long-running tasks: No timeout limits on execution duration
- Execution guarantees: At-least-once delivery with idempotency controls
The architecture now resembles Temporal’s workflow engine, but the SDK is TypeScript-native and the deployment model is fully managed. You write async functions, the platform handles replay and recovery.
Execution Model and State Persistence
Trigger.dev serializes task state at checkpoint boundaries. When you call await on an external API or database operation, the platform captures the result and stores it. If the process dies, the next execution resumes from the last checkpoint with the same inputs and prior results.
export const processOrder = task({
id: "process-order",
run: async ({ orderId }: { orderId: string }) => {
// Checkpoint 1: Fetch order
const order = await db.orders.findUnique({ where: { id: orderId } });
// Checkpoint 2: Charge payment
const charge = await stripe.charges.create({
amount: order.total,
currency: "usd",
customer: order.customerId,
});
// Checkpoint 3: Update inventory
await db.inventory.decrement({
where: { productId: order.productId },
data: { quantity: order.quantity },
});
return { orderId, chargeId: charge.id };
},
});
If the process crashes after charging the payment but before updating inventory, the replay skips the Stripe call (already checkpointed) and retries the inventory update. This prevents double-charging while ensuring the workflow completes.
Retry Semantics and Failure Boundaries
Trigger.dev distinguishes between transient failures (network timeouts, rate limits) and permanent failures (invalid input, business logic errors). The platform retries transient failures with exponential backoff. Permanent failures terminate the task and surface the error.
You control retry behavior per task:
export const fetchData = task({
id: "fetch-data",
retry: {
maxAttempts: 5,
minTimeoutInMs: 1000,
maxTimeoutInMs: 60000,
factor: 2,
},
run: async ({ url }: { url: string }) => {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
});
The platform tracks retry attempts and surfaces them in the dashboard. You see which steps failed, how many retries occurred, and the final error state. This matters for debugging multi-step workflows where a single flaky API call can cascade into hours of wasted execution.
Execution Isolation and Secret Management
Each task runs in an isolated container with its own environment variables and secrets. The platform injects secrets at runtime using a key-value store encrypted at rest. You reference secrets by name in your task code:
export const sendEmail = task({
id: "send-email",
run: async ({ to, subject, body }) => {
const apiKey = process.env.SENDGRID_API_KEY;
await sendgrid.send({ to, subject, body, apiKey });
},
});
Secrets never appear in logs or task payloads. The platform rotates encryption keys and supports secret versioning. If you update a secret, in-flight tasks continue using the old version until they complete.
Execution isolation also prevents resource contention. Each task gets a dedicated CPU and memory allocation. Long-running tasks don’t starve short-lived ones. You configure concurrency limits per task to control how many instances run in parallel.
Observability and Debugging Long-Running Workflows
The dashboard shows real-time execution traces for every task. You see:
- Start and end timestamps for each checkpoint
- Input and output payloads at each step
- Retry attempts and failure reasons
- Total execution time and resource usage
For workflows that span hours or days, this visibility is critical. You can inspect a task that started yesterday, see which API calls succeeded, and identify the exact step that failed. The platform preserves execution history for 30 days by default.
Structured logging integrates with the trace view. You call logger.info() inside your task, and the logs appear alongside the execution timeline. This beats grepping CloudWatch or Datadog when debugging a multi-step workflow that touches five external APIs.
TypeScript SDK and Closure Serialization
Trigger.dev’s SDK handles async context and closure serialization automatically. You write normal TypeScript functions with async/await. The platform instruments your code at build time to insert checkpoint boundaries.
The SDK serializes function arguments and return values using JSON. Complex objects (Dates, Maps, Sets) require manual serialization. The platform throws a runtime error if you try to checkpoint a non-serializable value.
export const processData = task({
id: "process-data",
run: async ({ items }: { items: Array<{ id: string; value: number }> }) => {
const results = new Map<string, number>();
for (const item of items) {
const processed = await externalAPI.process(item);
results.set(item.id, processed.value);
}
// Convert Map to object for serialization
return Object.fromEntries(results);
},
});
The SDK also handles task composition. You can call one task from another, and the platform tracks the dependency graph. If a child task fails, the parent task retries from the point of failure.
Deployment Shape and Scaling Model
Trigger.dev runs as a fully managed service. You push your TypeScript code to the platform using the CLI. The platform builds a container image, deploys it to a Kubernetes cluster, and routes task invocations to available workers.
Scaling happens automatically based on queue depth. If 100 tasks arrive simultaneously, the platform spins up additional workers to handle the load. Workers scale down to zero when idle. You pay per execution time, not for idle capacity.
For self-hosting, the platform provides Docker images and Kubernetes manifests. You run the control plane (API server, scheduler, dashboard) and workers in your own infrastructure. The architecture separates the control plane from execution workers, so you can scale them independently.
Comparison: Trigger.dev vs. Temporal vs. Inngest
| Feature | Trigger.dev | Temporal | Inngest |
|---|---|---|---|
| Language | TypeScript | Go, Java, Python, TypeScript | TypeScript |
| Deployment | Managed or self-hosted | Self-hosted (complex) | Managed |
| State Model | Checkpoint-based | Event sourcing | Checkpoint-based |
| Retry Control | Per-task config | Per-activity config | Per-step config |
| Observability | Built-in dashboard | Temporal UI (requires setup) | Built-in dashboard |
| Learning Curve | Low (async/await) | High (workflows vs. activities) | Low (async/await) |
| Execution Guarantees | At-least-once | Exactly-once | At-least-once |
Temporal offers stronger consistency guarantees (exactly-once execution) but requires running a multi-component cluster (frontend, history service, matching service, worker pools). Trigger.dev and Inngest trade exactness for operational simplicity.
Likely Failure Modes
Checkpoint overhead: Serializing state at every await adds latency. Tasks with tight loops that checkpoint hundreds of times per second will see performance degradation. Solution: batch operations or use in-memory state for hot paths.
Non-deterministic code: If your task reads Date.now() or Math.random() and uses the result in control flow, replay will produce different outcomes. The platform can’t detect this at build time. Solution: pass timestamps and random seeds as task inputs.
Secret rotation during execution: If you rotate a secret while a task is running, the next checkpoint will fail because the old secret is invalid. Solution: version secrets and allow tasks to complete with the version they started with.
Queue backlog during outages: If an external API goes down for hours, tasks will retry and accumulate in the queue. When the API recovers, the burst of retries can overwhelm it. Solution: configure rate limits and exponential backoff per task.
Technical Verdict
Use Trigger.dev when:
- You’re building in TypeScript and want durable execution without learning Temporal’s workflow/activity model
- You need long-running tasks (hours or days) with automatic retries and state persistence
- You want a managed service that scales to zero and bills per execution time
- You’re orchestrating AI agents, media processing pipelines, or multi-step API workflows
Avoid Trigger.dev when:
- You need exactly-once execution guarantees (financial transactions, ledger updates)
- Your tasks require sub-second latency (real-time bidding, high-frequency trading)
- You already run Temporal in production and have invested in its operational model
- You need polyglot support (Python, Go, Java workers in the same cluster)
The platform fills a real gap between serverless functions (no durability) and Temporal (high operational complexity). For TypeScript teams building agent orchestration or workflow automation, it’s a pragmatic choice that exposes the right primitives without forcing you to run a distributed database.