Bubble API Workflows to Trigger.dev: How to Migrate Async Work
Mapping Bubble's API workflows, scheduled workflows, and recursive workflows onto Trigger.dev tasks. The patterns, the gotchas, and what you gain.
Bubble's API workflows quietly grew into the background-job system for half the apps that use them. Scheduled workflows on a Date, recursive workflows for batch processing, API workflows triggered by webhooks - they all become "async work" the moment your app crosses any meaningful scale.
When you migrate to code, this entire layer needs somewhere to live. The wrong answer is "cron jobs and ad-hoc queue code." The right answer, for our migrations at least, is Trigger.dev - a managed service for background, scheduled, and retryable work, with first-class TypeScript and proper observability.
This article maps the three Bubble patterns to Trigger.dev equivalents, with the gotchas you'll hit during the migration.
The three Bubble patterns
Before we get to the mapping, name the patterns you actually have in your Bubble app:
1. API workflows triggered by other workflows
The most common Bubble pattern. A frontend workflow does some work, then "schedules an API workflow" to do follow-up work in the background. Examples: send a welcome email after signup, generate a thumbnail after image upload, recalculate a denormalised count after a record changes.
2. Scheduled workflows on a date
Workflows that run at a specific future time, often "the next time the booking starts" or "X days after the user signs up." Bubble lets you schedule against a record reference, which makes this pattern very natural.
3. Recursive workflows for batch processing
The classic Bubble pattern for processing more than a few records at once. You write a workflow that processes one record and then schedules itself again for the next record. It's how most Bubble apps do "process all orders from yesterday" or "send daily digest emails."
What you gain from Trigger.dev
Before mapping the patterns, let me explain why we picked Trigger.dev as the target rather than rolling your own.
You gain four things that Bubble's API workflows never had:
- Idempotency by design. Each task run has a unique key. If your code accidentally enqueues the same task twice, only one runs.
- Real retries with exponential backoff. Bubble retries failed workflows by re-running them - they fail again, you eat the WU cost, the queue backs up.
- Observability. A real dashboard showing every run, its state, its arguments, its logs. You can debug a failed task in 60 seconds instead of grep-ing through workflow logs.
- Type safety. Tasks are TypeScript functions with typed inputs. The compiler catches the bugs that previously only surfaced at 3am.
These aren't theoretical. They're the four things that, at Ohana, made the difference between "we can ship money-moving features confidently" and "we hold our breath every release."
Mapping the patterns
Bubble: scheduled API workflow → Trigger.dev: task with delay
The simplest mapping. A Bubble workflow that schedules another workflow becomes a Next.js server action (or another task) that calls yourTask.trigger({ delay: '5 minutes' }).
The big difference: Trigger.dev's delay is reliable. Bubble's delayed workflows occasionally don't fire when they should, or fire late under load. Trigger.dev's job is to be a queue, so the SLA is much higher.
Bubble: scheduled workflow on a date → Trigger.dev: task with absolute timestamp
Same idea, with an absolute timestamp instead of a relative delay. Pass a JavaScript Date to yourTask.trigger({ executeAt: date }) and Trigger.dev fires the task at that moment.
The gotcha: in Bubble, you can update or cancel a scheduled workflow by canceling the API workflow ID. Trigger.dev gives you a run ID at trigger time; cancellation requires storing that ID somewhere you can find later.
In practice we add a small scheduled_runs table keyed on the business entity (booking, user, whatever) and store the Trigger.dev run ID. When the entity changes, we cancel the old run and trigger a new one.
Bubble: recursive workflow → Trigger.dev: batched task
This is the biggest mental shift. In Bubble, processing 10,000 records means writing a workflow that does one and schedules the next. In Trigger.dev, you don't recurse - you batch.
The pattern is:
- A "coordinator" task that lists work items (e.g. "all orders from yesterday")
- The coordinator triggers a "worker" task for each item, with a batch ID
- Each worker processes one item independently, with full retry semantics
- An optional "completion" task fires when all workers in the batch finish
The advantages over Bubble's recursive pattern: workers run in parallel (up to the concurrency limit you set), failures of one worker don't break the whole batch, and you can see progress in real time on the dashboard.
The gotcha: if your previous recursive workflow had ordering dependencies ("process record A before record B"), you need to make those explicit. Either chain them sequentially, or model the dependency in the task input.
The "workflow body" mental model
Bubble workflows are sequences of "actions" - send email, update record, schedule workflow. Trigger.dev tasks are TypeScript functions.
This is a feature, not a bug. The TypeScript function has:
- Variables you can name (instead of "Result of Step 3")
- Conditional logic that reads like prose
- Type-safe access to your database via Drizzle
- Real
try/catchfor error handling - The ability to call other functions, not just other tasks
In practice, most "10-step Bubble workflows" become 30-50 lines of TypeScript that read like the comments you wished the workflow had.
The migration order during a slice cutover
When you're migrating slice by slice, the async work for that slice usually moves over with it. The general order:
- Find every Bubble workflow that touches the slice's Data Types
- Categorise: synchronous frontend workflows vs. async (API/scheduled/recursive)
- The synchronous ones become server actions in the new app
- The async ones become Trigger.dev tasks
- Wire the new app to trigger the new tasks
- Once the new path is live and observed, retire the Bubble workflow
The risky moment is step 5-6. For a short period, both Bubble and Trigger.dev might process the same logical event. Make tasks idempotent - keyed on a stable business ID like booking-paid:{bookingId} - so the duplicate runs don't cause damage.
Patterns we use most often
A few patterns we use on every Bubble-to-code migration:
Webhook receivers as tasks, not endpoints
When a webhook comes in (Stripe, your supplier API, anything else), don't process it in the receiving HTTP handler. Acknowledge the webhook fast, enqueue a Trigger.dev task with the payload, return 200. The task processes the work with full retry semantics.
This means a Stripe webhook that previously took 4 seconds to handle (and occasionally timed out) now returns 200 in 30ms and the heavy work runs in the background.
Scheduled batch jobs as cron tasks
Trigger.dev has first-class cron syntax. A daily digest email job becomes:
cron: "0 9 * * *", run: send daily digests
Versus Bubble's pattern where you have to seed the recursive workflow each day from a manual or scheduled trigger. The cron version is one line shorter and won't silently fail to fire.
Long-running operations with progress
If the operation takes more than 30 seconds (CSV imports, batch reports, etc), Trigger.dev gives you a metadata field you can update during execution. Your frontend can poll the run ID and show a progress bar.
This is the kind of UX that's basically impossible in pure Bubble.
The gotchas
The five things that bite teams during this migration:
- You'll find Bubble workflows that should never have been async. A "background" workflow that the user is actually waiting for. Make these synchronous in the new app.
- You'll find workflows that are async only because of how Bubble handles errors. Move them to synchronous server actions with proper error handling.
- Some scheduled workflows reference no-longer-existing records. Decide what to do with these on cutover - cancel, ignore, or migrate.
- Recursive workflows that have been running for weeks. Drain them before cutover or they'll keep running and writing to data that's now read-only.
- Webhook URLs that point at Bubble. Update them at cutover, not before; otherwise you'll lose webhooks during the transition.
What to do next
If you're planning a migration, the migration playbook PDF covers how this fits into Phase 4 of our six-phase approach.
If you want a senior pair of eyes on your specific async-work patterns before you commit, book a 30-minute discovery call - we can usually tell you in 20 minutes how much of your Bubble API workflow inventory translates cleanly.
Read next: Bubble to Next.js migration guide and Slice-by-slice vs big-bang migration.
Got a Bubble or Canvas app you’d like a second pair of eyes on?
30-minute discovery call. We’ll look at your app live and tell you honestly what we’d do next.
Or grab the Bubble migration playbook PDF.