Trigger.dev started as a “developer-first Zapier alternative” in February 2023 (745 HN points, 190 comments). By October 2023, the positioning shifted to “Temporal alternative for TypeScript devs” (172 points). That evolution exposes a real architectural divide: webhook-based automation platforms (Zapier, Make, n8n) solve different problems than durable execution engines (Temporal, Inngest, Trigger.dev).
The distinction matters for agent orchestration. Visual workflow builders excel at connecting SaaS APIs. Code-first platforms handle long-running tasks, partial failures, and stateful retries. If your agent needs to wait 12 hours for a human approval, retry a flaky LLM call with exponential backoff, or fan out 500 parallel tasks without hitting rate limits, you need durable execution primitives.
The Webhook Problem
No-code platforms treat workflows as stateless event chains. A trigger fires, steps execute in sequence, and the workflow completes or fails. State lives in the platform’s database, not your code. Retries happen at the step level, not the execution level.
This works until you hit:
- Timeouts: Most webhook-based platforms cap execution at 30-300 seconds. Long-running tasks (video transcoding, batch processing, multi-step agent loops) require workarounds like splitting workflows or polling external state.
- Partial failures: If step 7 of 10 fails, you cannot resume from step 7. You restart the entire workflow or manually fix state.
- Rate limits: Parallel execution requires careful queue management. Visual builders expose concurrency controls, but you cannot dynamically throttle based on runtime conditions.
- Observability gaps: Logs show step inputs and outputs, but you cannot trace causality across multiple workflow invocations or correlate failures with external events.
Code-First Execution Model
Trigger.dev (and Temporal, Inngest, Restate) invert the model. You define tasks as TypeScript functions. The platform handles:
- Durable execution: Tasks can run for hours or days. The runtime persists state at await boundaries, so failures resume from the last checkpoint.
- Automatic retries: Exponential backoff, jitter, and custom retry policies live in code, not UI configuration.
- Concurrency primitives: Queues, rate limiters, and semaphores are first-class constructs. You control parallelism with code, not dropdown menus.
- Versioning: Task definitions are code artifacts. You deploy new versions, run migrations, and roll back with Git semantics.
Here’s a minimal Trigger.dev task that demonstrates the execution model:
import { task } from "@trigger.dev/sdk/v3";
export const processDocument = task({
id: "process-document",
retry: {
maxAttempts: 5,
factor: 2,
minTimeout: 1000,
maxTimeout: 60000,
},
run: async (payload: { documentId: string }) => {
// Step 1: Fetch document (retries on network failure)
const doc = await fetchDocument(payload.documentId);
// Step 2: Extract text (checkpoint before expensive operation)
const text = await extractText(doc.url);
// Step 3: Call LLM (retries on rate limit or transient error)
const summary = await llm.summarize(text, {
maxTokens: 500,
temperature: 0.3,
});
// Step 4: Store result (checkpoint before side effect)
await db.documents.update(payload.documentId, { summary });
return { documentId: payload.documentId, summaryLength: summary.length };
},
});
The runtime automatically:
- Persists state after each
await(checkpoints). - Retries the entire task on failure, resuming from the last checkpoint.
- Exposes logs, traces, and spans for each step.
- Handles concurrency limits if you configure a queue.
State Persistence and Checkpointing
The key architectural difference is where state lives. In webhook platforms, state is external. You store intermediate results in a database, cache, or the platform’s internal store. In code-first platforms, state is implicit in the call stack.
When a Trigger.dev task awaits an async operation, the runtime:
- Serializes the call stack and local variables.
- Writes the checkpoint to durable storage (Postgres, S3, or platform-managed).
- Releases the worker.
- On resume, deserializes the checkpoint and continues execution.
This enables:
- Long waits: A task can
await sleep(24 * 60 * 60 * 1000)(24 hours) without holding a worker or connection. - Human-in-the-loop: Pause execution, wait for an external event (webhook, user approval, manual intervention), then resume.
- Partial retries: If step 7 fails, the runtime retries from step 7, not step 1.
The trade-off: you cannot use non-serializable state (open file handles, database connections, closures over external scope). The runtime must be able to reconstruct the call stack from a checkpoint.
Event Ingestion and Queuing
Trigger.dev exposes three ingestion patterns:
- HTTP triggers: POST to an endpoint, task executes immediately.
- Scheduled triggers: Cron-style schedules with durable guarantees (no missed executions).
- Event triggers: Subscribe to webhooks, database changes, or message queues.
Internally, all triggers enqueue a task execution. The queue layer handles:
- Concurrency limits: Max N tasks running concurrently per queue.
- Rate limiting: Max M tasks per second, with token bucket or sliding window algorithms.
- Priority: High-priority tasks jump the queue.
- Deduplication: Idempotency keys prevent duplicate executions.
Here’s how you configure a queue with concurrency and rate limits:
export const highPriorityQueue = queue({
name: "high-priority",
concurrency: 10, // Max 10 tasks running at once
rateLimit: {
limit: 100, // Max 100 tasks
window: "1m", // Per minute
},
});
export const urgentTask = task({
id: "urgent-task",
queue: highPriorityQueue,
run: async (payload) => {
// Task logic
},
});
The queue is a first-class construct. You can inspect queue depth, pause/resume queues, and dynamically adjust concurrency at runtime.
Observability and Debugging
Visual workflow builders show step-by-step execution logs. Code-first platforms expose distributed tracing. Trigger.dev integrates OpenTelemetry, so every task execution generates:
- Spans: One span per function call, with timing and metadata.
- Logs: Structured logs attached to spans.
- Traces: End-to-end trace across task invocations, external API calls, and database queries.
You can:
- Search logs by task ID, execution ID, or custom attributes.
- Filter traces by duration, error status, or tag.
- Correlate failures across multiple tasks (e.g., a parent task that fans out 100 child tasks).
The debugging workflow differs from visual builders:
| Aspect | Visual Builder (Zapier) | Code-First (Trigger.dev) |
|---|---|---|
| Failure inspection | Click failed step, view inputs/outputs | Query traces by execution ID, inspect spans |
| Retry logic | Configure in UI, limited to step-level | Define in code, execution-level with custom backoff |
| State inspection | View platform database or external store | Inspect serialized checkpoint in storage |
| Local testing | Mock webhooks, limited replay | Run tasks locally with dev server, full replay |
| Versioning | No versioning, edits apply immediately | Git-based versioning, deploy new versions |
Deployment Shape
Trigger.dev runs as a managed service or self-hosted. The architecture has three layers:
- API server: Handles HTTP requests, schedules tasks, manages queues.
- Worker pool: Executes tasks, checkpoints state, reports telemetry.
- Storage layer: Postgres for metadata, S3 (or compatible) for checkpoints and logs.
Self-hosting requires:
- Postgres 14+ with JSONB support.
- S3-compatible object storage (MinIO, Cloudflare R2, AWS S3).
- Redis for queue coordination (optional but recommended).
- Worker nodes with Node.js 18+ or Bun.
The managed service abstracts this. You push code, the platform handles deployment, scaling, and observability.
When Code-First Makes Sense
Use Trigger.dev (or similar) when:
- Tasks run longer than 5 minutes: Webhook platforms time out. Code-first platforms checkpoint and resume.
- Failures are common: LLM rate limits, flaky APIs, transient network errors. Automatic retries with exponential backoff are table stakes.
- State is complex: Multi-step workflows with branching logic, loops, or conditional retries. Visual builders become unreadable.
- Observability matters: You need distributed tracing, structured logs, and correlation across task invocations.
- Versioning is required: You deploy multiple versions, run A/B tests, or need rollback guarantees.
Avoid code-first platforms when:
- Tasks are simple: One-step automations (send email on form submit) are faster to build in visual tools.
- Non-technical users manage workflows: Visual builders are self-service. Code requires deployment pipelines.
- Execution time is under 30 seconds: Webhook platforms are simpler and cheaper for short-lived tasks.
Failure Modes
Code-first platforms introduce new failure modes:
- Checkpoint corruption: If serialization fails (non-serializable state, circular references), the task cannot resume. You lose in-flight work.
- Version skew: Deploying a new task version while old executions are in-flight can cause deserialization errors. The platform must handle version migrations.
- Storage exhaustion: Checkpoints and logs consume storage. Long-running tasks with large state can exhaust disk or S3 quotas.
- Worker starvation: If all workers are busy with long-running tasks, new tasks queue indefinitely. You need autoscaling or manual capacity planning.
Mitigation strategies:
- Validate serialization: Test checkpointing in dev. Use
JSON.stringifyto verify state is serializable. - Version migrations: Write migration scripts for breaking changes. The platform should support running multiple versions concurrently.
- Checkpoint pruning: Expire old checkpoints after task completion. Configure retention policies.
- Queue monitoring: Alert on queue depth, worker utilization, and task latency. Autoscale workers based on queue depth.
Technical Verdict
Use it if your workflow depends on checkpointing and partial retry semantics. A multi-step research agent that calls Perplexity, waits 6 hours for a human to review findings, then calls Claude to synthesize a report cannot run in Zapier (timeout) or survive a worker crash without checkpoints. The same applies to batch video processing where step 4 (thumbnail generation) fails due to a transient S3 error: you want to retry from step 4, not re-encode the entire video. Trigger.dev’s automatic checkpointing after each await makes this the default behavior.
Use it if you need distributed tracing across fan-out tasks. An agent that spawns 200 parallel web scraping tasks, aggregates results, then calls an LLM requires correlation between parent and child executions. OpenTelemetry spans let you filter by task ID, trace causality, and debug which of the 200 tasks caused the rate limit error. Visual builders show individual step logs but cannot correlate across invocations.
Use it if retry logic must adapt to runtime conditions. A task that calls the Claude API with exponential backoff (1s, 2s, 4s, 8s, 16s) but switches to GPT-4 after 3 consecutive rate limit errors requires code-level control. You cannot express “retry 5 times, then call a different API” in a Zapier dropdown. Trigger.dev’s retry config is TypeScript, so you can inject custom logic (check error type, inspect response headers, consult a circuit breaker).
Avoid it if your workflow is a single API call triggered by a webhook. A Slack message that posts to a Google Sheet does not need checkpointing, retries, or distributed tracing. Zapier handles this in 2 minutes with zero code. Trigger.dev requires a TypeScript function, deployment pipeline, and worker infrastructure. The overhead is not justified.
Avoid it if non-technical users need to edit workflows. Visual builders are self-service. A marketing team can add a new step to a Mailchimp automation without touching code. Trigger.dev requires a developer to write TypeScript, commit to Git, and deploy. If your team lacks TypeScript expertise or prefers UI-based editing, stick with n8n or Make.
Avoid it if execution time is under 30 seconds and failures are rare. A daily cron job that fetches RSS feeds and sends an email digest does not need durable execution. Webhook platforms are simpler and cheaper for short-lived, low-stakes tasks. Trigger.dev’s checkpointing overhead (serialize state, write to S3, deserialize on resume) adds latency that only pays off for long-running or failure-prone workflows.
The shift from “Zapier alternative” to “Temporal alternative” reflects this. Trigger.dev competes with Temporal and Inngest for agent orchestration, not with Zapier for SaaS integration. If your agent needs to survive worker crashes, retry flaky LLM calls across hours, or trace failures across hundreds of parallel tasks, the checkpointing and observability primitives justify the complexity. If you are connecting two SaaS tools, they do not.