Bootcamp
Custom events, conditionals and workflow timing in Bubble
Photo by Scott Webb on Unsplash
Every Bubble developer eventually meets the bug that only happens sometimes. The workflow runs cleanly in testing, runs cleanly again, and then one time in twenty it quietly does the wrong thing. Bugs like that are rarely random. They come from how workflows are organised, how conditionals overlap, and above all from timing: what actually runs in order, and what merely looks like it does.
A custom event is a named, reusable workflow that other workflows trigger. It behaves like a function in code: it accepts parameters, performs one well-defined task, and can return a value to the workflow that called it.
Where workflows live, and how to keep them findable
Every page in a Bubble app has its own workflows, and so does every reusable element (backend workflows are a separate world for a later module). The Workflows tab shows whichever page you’re editing, and two small features do a lot of the maintainability work: workflow folders, which let you categorise workflows and zoom in on one group at a time instead of scanning everything, and event colours. Colour is worth systematising: one convention our team uses is light grey for complete, blue for in progress, red for candidates for deletion.
Two words underpin everything here. Events are what trigger workflows: the general ones (page is loaded, do every five seconds, do when a condition is true, when an unhandled error occurs, user is logged in or logged out), the element ones (an input’s value is changed, an element is clicked, an element has an error running a workflow) and custom events. Actions are the steps that then execute, across categories like account, navigation, data, email, payments, analytics and element actions. Plugins add both events and actions, and some actions only appear once their element is on the canvas: if you can’t find the show-alert action, there’s no alert element on the page yet.
The final piece of framing is the client-server model. Some actions run entirely in the browser (set state, show, hide, animate, scroll to, set focus, reset inputs, changing URL parameters) and feel instantaneous. Others (logging in, creating data, reading data, sending an email) need a round trip to the server, which is why they take a beat. Hold onto that split; it drives every timing behaviour below.
Custom events are functions
You create a custom event from the custom category of the Workflows tab, then fire it from another workflow with the trigger a custom event action, which runs it synchronously, in sequence with the calling workflow. Its sibling, schedule a custom event, makes a different promise: it fires the event off to run in parallel, with no timing relationship to the steps that follow. A custom event holds any series of actions, can accept parameters (say, a Contact Us Submission the event refers to throughout its steps), and can now return values to the caller via the return data action, a recent addition that closes one of Bubble’s longest-standing gaps.
If you’ve done any programming, or even used Excel, the right mental model is the function. SUM(A1:A10) takes an input range and returns one value; a custom event should be the same shape. Three principles follow:
- One task per event. This is separation of concerns: each module of your software does one job only, which keeps the whole app modular and easy to work on.
- Meaningful names.
create customer,calculate order value,go to next step. The name should describe the task so completely that you can navigate your app by names alone. - Build once, trigger from many places. Different events across a page can all trigger the same custom event, so the actions exist exactly once.
Here’s what that looks like in practice. Imagine a button whose workflow runs a long string of steps: get the data, get the customer info, create the order, make changes to all the order line items, process the payment, store the payment details back on the order, send an email to the user, send an email to the admin, log an event in Google Analytics, log an event in your own event log. Fifteen or twenty steps trailing along after one click is hard to make sense of. Refactor it into four triggers: initialise, place order, process payment, notify user, each a custom event holding its own handful of actions. The same work happens, but the workflow now reads at a glance, and you know exactly where to look when the order-creation logic needs a change.
Custom events also work inside reusable elements, triggered from the parent page. One pattern we use constantly is an analytics reusable with no real UI at all, just workflows: log the page load, grab the IP address, store any unhandled error to an error log, record events and page navigations. Drop it on any page and trigger its custom events from that page’s workflows. The same trick suits CRUD operations: if creating a customer involves a chain of updates and some complicated searches, keep that as one workflow inside a reusable rather than replicating it in three or four places. Duplicate that logic and a later change will almost certainly miss one copy, leaving a bug that appears only occasionally.
Two more properties matter later: a conditional placed on the trigger applies to everything inside the event, and the actions inside a custom event cannot start until it is triggered. The first tidies your logic; the second turns out to be a timing tool.
Conditionals: on the event or on the action?
The Only when field appears on both events and actions, and the age-old Bubble question is which one to use.
Put the conditional on the event when the action would change the value being evaluated. The classic case is a sort toggle: build two events, one that runs when the sort field is ascending and sets it to descending, and one that does the reverse. If you instead build one workflow with two conditional actions, the first action flips the value, the second action’s condition then also becomes true, and they cancel each other out.
Conditionals on actions suit a workflow with two possible logic paths: if this, run steps A, B, C; if that, run D, E, F. That’s workable for five steps, but the Bubble manual’s general rule stands: when a trigger can have multiple results based on conditions, it is safer to create multiple workflows with the conditions at the event level than one workflow with all possible actions conditioned individually. Our version of the principle: multiple events with conditionals and few actions beat one event with many conditional actions.
Beyond placement, write explicit logic. Conditions on parallel paths should be mutually exclusive, so no two paths can ever both be true. Overlapping conditions create a matrix of combinations that multiplies the outcomes until nobody can reason about the logic later. That’s spaghetti, and spaghetti is always a signal to refactor. The same applies to the single monster expression, the when A + B is empty or C or D and D ≥ Search for XYZ:count variety: break it into smaller parts. Often the cleanest refactor is a pair of well-named custom events (create order when..., update order when...) because the conditional is then written once on each trigger and covers every step inside, instead of being duplicated across actions. Don’t repeat yourself.
Finally there’s terminate this workflow, an action that stops every step after it, usually with its own Only when. It’s a legitimate control-flow tool: check a condition early and bail out, or cut a workflow short while debugging. Use it with care though. A terminate buried mid-sequence is easy for a future editor to move or reorder without realising its importance, so where a well-named custom event creates clarity, a terminate can create confusion. It also has one surprising exception, covered below.
What actually runs in order
Sequential execution means steps run one after another, each waiting for the last. Asynchronous execution means they don’t: everything fires, and each thing finishes whenever it finishes. The web is asynchronous by design: a page requests all its data at once rather than queueing, because network transmission is the slow part. Some requests return in a tenth of a second; a Stripe charge or a PDF generation can take many seconds, and nothing else should wait behind them.
In Bubble that plays out at two levels.
Events run asynchronously. Nothing stops you creating two “page is loaded” events, or three events on the same button click. All of them fire, milliseconds apart, in no guaranteed order. So avoid putting multiple events on the same trigger unless each carries a distinguishing conditional (different redirects on page load, a checkbox’s checked versus unchecked behaviour). And never let one event depend on another’s output. The classic bug: page-load event A sets a custom state, page-load event B does a search where field = state A's value. There is no guarantee A finishes first. It will work 95 per cent of the time, and the 5 per cent will cost you dearly. Setting a state inside a reusable element and immediately referencing it is the same bug, harder to spot.
Actions usually run in order, but it isn’t guaranteed. A workflow is drawn as a linear list, but as the Bubble manual explains, some actions run in the browser, some on the server and some in both, and when a workflow mixes those categories, the firing order can differ from the visual order. You have three tools for forcing sequence:
- Reference the result of a previous step. Using
Result of step 1in step 2, even justResult of step 1's unique id is not emptyin theOnly when, both gives you the created data and forces step 2 to wait for step 1. Be precise about what references what: if step 3 references step 1 rather than step 2, then steps 2 and 3 can still race each other. - Wrap steps in a custom event. Actions inside a custom event cannot start until it’s triggered, so a triggered custom event acts as a checkpoint in the sequence. The manual recommends exactly this when a later step’s conditional depends on data manipulated in an earlier one.
- Know the Schedule API workflow exception. A scheduled API workflow fires as soon as the event triggers, even if it’s the last step, and even if a
terminate this workflowsits before it. This is well documented and catches almost everyone once. To hold it back, put it inside a custom event, which can’t run until every earlier step has finished.
Then there’s add a pause, which deserves standing suspicion. It pauses for a set number of milliseconds (1,000 by default), works only on the front end, and gives no guarantee that asynchronous work finishes within the pause. A slow connection, or a Stripe card token taking longer than usual, and your next step tries to charge a card whose token hasn’t arrived. A pause is fine for user experience. It is never a fix for a timing problem before a critical data action.
One last behaviour to know: Bubble optimises by caching searches. Run the same search twice in one workflow, with a data change in between, and the second search may simply return the cached first result rather than the update you expected. The fixes are the same: reference the result of the earlier step, or move the later steps into a custom event.
Timing bugs, the workflows that work most of the time, are the worst class of bug in Bubble: we have personally lost entire days to them. If something fails one time in twenty during testing, don’t push it live and don’t hand it over. Find the cause, which will almost always be one of the patterns above, and refactor. Committing to reliability up front makes you ten times faster as a developer.
Try it yourself
Open the longest workflow in your app and count the steps. If it’s more than six or seven, refactor it into three or four custom events with names that describe their tasks, and move each conditional onto the trigger so it’s written exactly once. Then audit your page-load events: if any event reads a state or value that another event on the same trigger sets, you’ve found a timing bug before your users did.
This skill in the AI era
Strip away the visual editor and this module is simply programming’s core curriculum: functions with parameters and return values, mutually exclusive branching, race conditions and caching. Those concepts are precisely what you need when directing AI tools, because when Claude Code writes asynchronous code for you, the developer’s job is recognising that “works most of the time” is a design flaw, not bad luck, and asking for the sequencing guarantee by name. It’s the foundation we build on in our Claude Code development work, and a big part of why Bubble skills transfer so cleanly to AI-assisted coding.
If this sparked something, let's talk.
No pitch, no pressure — just a conversation about what you're working on.
Let's talkRelated posts
Bootcamp
A simple approach to data modelling
A four-step method for designing a database schema: identify entities, map relationships, choose fields. Taught through the CRM data model built in Bubble.
Bootcamp
Module 5 — Application architecture: pages, states and reusable pieces
Single-page vs multi-page apps, URL parameters, custom states, reusable elements — and a big practical building a main template with an expanding navigation drawer.
Bootcamp
API calls as actions vs data (and the planning checklist)
When to use Bubble API calls as actions versus data sources, a one-to-many auth pattern, and the planning checklist that keeps integrations from failing silently.