How to Rebuild Your Bubble App's Workflows in Code
A practical, step-by-step guide to converting Bubble workflows into server actions, API routes, cron jobs, and JavaScript logic — with concrete examples for every common pattern.
MigrateLab Team
Migration Experts

Understanding Bubble Workflows
Bubble workflows are the backbone of every Bubble application. They're sequences of actions triggered by events: button clicks, page loads, input changes, API calls, scheduled triggers. If your Bubble app does anything — creates a record, sends an email, redirects a user, makes an API call — it does it through a workflow.
The migration challenge is that workflows combine several concerns that, in code, are handled by different mechanisms. A single Bubble workflow might include database writes, conditional logic, API calls, email sends, and page navigation — all in one visual sequence. In code, these get separated into their appropriate layers: database operations in your ORM, business logic in service functions, API calls in dedicated modules, and navigation in your router.
This separation is actually an improvement. Bubble's "everything in one workflow" approach becomes unmaintainable at scale — debugging a 20-step workflow with nested conditionals in a visual editor is significantly harder than reading a 50-line function. But the initial translation requires understanding how each workflow pattern maps to code.
Pattern 1: Button Click → Server Action
The most common Bubble workflow starts with "When Button X is clicked." In Next.js, this becomes a server action — a function that runs on the server when called from the client.
Bubble Pattern
In Bubble, you'd have a workflow triggered by a button click that creates a new "Task" with fields populated from input elements:
- Trigger: When "Create Task" button is clicked
- Step 1: Create a new thing (Task)
- Step 2: Set field "title" = Input Title's value
- Step 3: Set field "description" = Input Description's value
- Step 4: Set field "assigned_to" = Current User
- Step 5: Show "Success" popup
Next.js Equivalent
In Next.js, you create a server action that handles the database write and return the result to the client component. The form component calls the server action on submit, and displays a success message based on the response. The server action validates the input, creates the database record using Prisma, and returns a success or error result.
The advantages are immediate: you get type safety (TypeScript ensures your fields match your schema), proper validation (Zod or Yup schemas catch invalid data before it reaches the database), and error handling (try/catch blocks provide meaningful error messages instead of Bubble's generic error popups).
Pattern 2: Conditional Logic → JavaScript If/Else
Bubble's "Only when" conditions are attached to workflow steps. They determine whether a step executes based on element states, user properties, or data values. In code, these become standard JavaScript conditionals.
Bubble Pattern
A common Bubble pattern: different actions based on user role.
- When "Submit" button is clicked:
- Step 1: Only when Current User's role is "admin" → Navigate to Admin Dashboard
- Step 2: Only when Current User's role is "member" → Navigate to Member Dashboard
- Step 3: Only when Current User's role is "guest" → Show "Upgrade" popup
Next.js Equivalent
In code, this becomes a straightforward if/else or switch statement in your server action or event handler. You query the user's role from the session, then use a switch statement to redirect to the appropriate dashboard or return an "upgrade required" response for guests.
What makes the code version better: you can easily add new roles, combine conditions with boolean logic (AND, OR, NOT) without Bubble's visual gymnastics, and test the logic with unit tests. Bubble's visual conditions become increasingly opaque as they get complex — nested conditions in the visual editor are almost impossible to debug.
Pattern 3: Page Load Data Fetching → Server Components
Bubble loads data on page display using "Do a search for" expressions bound to elements. In Next.js, server components fetch data before the page renders — the user sees content immediately instead of waiting for client-side data loading.
Bubble Pattern
A Bubble page that displays a list of projects:
- Repeating Group's data source: "Do a search for Projects where Creator = Current User, sorted by Created Date descending"
- Each cell displays: Project's Name, Project's Status, Project's Created Date
- Empty state: "No projects yet" text visible when RG's list of Projects count is 0
Next.js Equivalent
In Next.js, this becomes a server component that queries the database directly. You define an async function that fetches projects from PostgreSQL using Prisma, filtering by the current user's ID and ordering by creation date. The component renders the data immediately — no loading spinner, no client-side fetch, no delay.
The performance difference is dramatic. Bubble's approach requires: page load → JavaScript execution → API call to Bubble backend → query execution → data transfer → render. The Next.js approach: server receives request → database query → HTML rendered → sent to browser. The user sees content 2-4 seconds faster because the data is fetched on the server and included in the initial HTML.
Pattern 4: Scheduled Workflows → Cron Jobs
Bubble's scheduled workflows run at specified intervals — daily report generation, weekly email digests, hourly data syncs. In code, these become cron jobs.
Bubble Pattern
A common Bubble scheduled workflow:
- Schedule: Every day at 9:00 AM
- Step 1: Search for Users where subscription_end < Current date/time
- Step 2: Make changes to list of Users → set status = "expired"
- Step 3: For each User in list → Send email "Your subscription has expired"
Next.js Equivalent
In Next.js, you create an API route that handles the subscription expiration logic and trigger it with a cron service. The API route queries for expired subscriptions, updates their status in a batch operation, and sends emails using your email provider's SDK (SendGrid, Resend, or Postmark).
For triggering the cron, you have several options:
- Vercel Cron: Add a cron configuration to your vercel.json that calls your API route on a schedule. Simple and integrated.
- External cron service: Services like cron-job.org or EasyCron call your API endpoint on a schedule. Works with any hosting provider.
- Node-cron: For self-hosted deployments, the node-cron package runs scheduled tasks within your application process. Simple and dependency-free.
The code version is more reliable because you have proper error handling, retry logic, and logging. If a Bubble scheduled workflow fails, you get a vague error in the logs. In code, you can implement exponential backoff for email sends, transaction rollback for database operations, and detailed error notifications via Slack or email.
Pattern 5: API Connector → Fetch / Axios
Bubble's API Connector plugin lets you call external APIs. It's one of Bubble's most-used features — and one of its most frustrating. The visual interface for configuring API calls is clunky, error-prone, and makes debugging difficult. Dynamic headers, request body manipulation, and response parsing all happen through Bubble's limited UI.
Bubble Pattern
Calling a payment API in Bubble:
- API Connector configured with endpoint URL, headers, and body template
- Workflow step: "Stripe - Create Charge" (API call)
- Dynamic parameters mapped from input elements
- Response parsed and stored in Bubble data type
Next.js Equivalent
In code, API calls become fetch or axios calls in your server actions or API routes. You use the Stripe SDK (or any provider's official SDK) which provides type-safe methods, automatic retries, and comprehensive error handling. Instead of Bubble's visual parameter mapping, you write a clear function that takes parameters, makes the API call, handles errors, and returns the result.
Official SDKs are vastly superior to Bubble's API Connector because they handle authentication, request signing, rate limiting, retries, and error handling automatically. They're also well-documented with examples for every use case. The Stripe Node.js SDK, for example, has 50+ method categories covering every possible operation — while Bubble's API Connector requires you to manually configure each endpoint.
Pattern 6: Custom Events → Event Emitters / Function Calls
Bubble's custom events are reusable workflow sequences that can be triggered from other workflows. They're Bubble's version of functions. In code, these become literal functions — or event emitters for loosely coupled communication between components.
Bubble Pattern
A custom event "Process Order":
- Custom event: "Process Order" (triggered from multiple buttons/workflows)
- Step 1: Create Shipment record
- Step 2: Update Order status to "processing"
- Step 3: Send confirmation email to Order's customer
- Step 4: Trigger webhook to warehouse system
Next.js Equivalent
This becomes a server-side function (a service function) that encapsulates the order processing logic. You create a processOrder function that takes an order ID, executes all four steps with proper error handling and database transactions, and returns a result. Any API route or server action that needs to process an order calls this function directly.
The function approach is better because it provides composability (functions can call other functions), testability (you can unit test processOrder independently), and transaction safety (wrap all database operations in a transaction so they either all succeed or all roll back).
Pattern 7: Data Triggers → Database Hooks / Middleware
Bubble has "Database Trigger Events" — workflows that fire when a record is created, modified, or deleted. In code, these become database hooks (with Prisma middleware or Drizzle hooks) or application-level event handlers.
Bubble Pattern
- When a new Order is created:
- Step 1: Send email to admin notification list
- Step 2: Update Metrics data type (increment total_orders)
- Step 3: If Order amount > $500, create VIP flag on Customer
Next.js Equivalent
You can implement this with Prisma middleware that intercepts create operations on the Order model, or more commonly, as logic in your createOrder service function. The service function creates the order, sends the notification, updates metrics, and applies VIP flags — all within a single, testable function.
For applications with many trigger-based operations, consider an event-driven architecture using a lightweight event bus. When an order is created, emit an "order.created" event. Separate handler functions listen for this event and perform their respective tasks. This keeps your code modular and prevents your order creation function from becoming a monolithic sequence of side effects.
Migration Strategy: Workflow by Workflow
Don't try to convert all workflows at once. Prioritize by business impact:
- Core user flows first. Registration, login, and the primary action your users take (creating a project, placing an order, booking an appointment). These workflows must work perfectly before anything else.
- Data mutation workflows second. Any workflow that creates, updates, or deletes data. These are critical for data integrity.
- Notification and email workflows third. Important but not blocking — you can launch without every notification in place and add them in the first week.
- Scheduled and background workflows last. These run behind the scenes. Convert them after your user-facing features are working.
For each workflow, the process is: read the Bubble workflow steps, write the equivalent code (with AI assistance for speed), test with real data, and verify the output matches what Bubble produced. Keep Bubble running in parallel during migration so you can compare results side by side.
| Feature | Bubble Workflow | Code Equivalent |
|---|---|---|
| Button click action | Visual workflow steps | Server action / API route |
| Conditional logic | "Only when" conditions | if/else / switch statements |
| Page load data | "Do a search for" | Server component + Prisma query |
| Scheduled task | Scheduled workflow | Cron job + API route |
| External API call | API Connector plugin | fetch / official SDK |
| Reusable logic | Custom events | Service functions |
| Data triggers | Database trigger events | ORM hooks / event emitters |
| Debugging | Step-by-step debugger (limited) | Console, debugger, stack traces |
Workflow Migration Priority Order
Migrate core user flows
Convert registration, login, and your primary user action (order, booking, project creation) first. These are the workflows your users interact with most and must be bulletproof.
Tip: Test each migrated workflow with real data from your Bubble export to verify identical behavior.
Migrate data mutation workflows
Convert all workflows that create, update, or delete records. Implement proper validation and error handling — this is your chance to fix the data integrity gaps Bubble allowed.
Tip: Wrap multi-step data operations in database transactions to prevent partial writes.
Migrate notification workflows
Convert email sends, in-app notifications, and webhook triggers. Use a dedicated email service (Resend, SendGrid) with proper templates.
Tip: Set up email previews (react-email) so you can see emails before sending — something Bubble doesn't offer.
Migrate scheduled and background workflows
Convert cron-based tasks like report generation, data cleanup, and subscription management. Implement proper logging and error handling with retry logic.
Tip: Add monitoring (Sentry, Axiom) to catch failures in background workflows — these run unattended and silent failures are common.
Need help migrating from Bubble? We handle the technical heavy lifting so you can focus on your business.