Article · Feb 13, 2026

Why I stopped trusting Bubble.io's list fields and re-query the database instead

Bubble.io list fields don't auto-update when children point at parents. Re-query the database, write the actual list back. Here is the pattern.

If you have built anything more complex than a single-table app on Bubble.io, you have probably hit the bug where a parent record renders an empty list of children even though the children clearly reference the parent. The repeating group sits empty. The Data API export shows zero tasks on the project. The list field stays unpopulated for hours. Meanwhile, a search expression like “Search for Tasks where Project = SomeProject” returns the correct seven children every time.

The cause is structural to how Bubble.io models relationships, and the fix is structural too: stop trusting the platform’s list field and re-query the database for the actual children, then write the result back to the parent. This pattern is what I call the Finalize Parents pass, and on a recent client migration it restored 722 parent-side relationships that had been silently broken.

This post is a deep-dive on the pattern that fixed silent failure #2 in my recent Bubble + n8n migration case study. The pattern generalizes: every platform with denormalized relationship fields has some version of this bug, and the fix shape is the same.

The Bubble.io list-field model, briefly

A list field on a Bubble data type stores an array of references to other things. The classic example is a Project type with a tasks field of type “list of Tasks.” Bubble’s documentation describes list fields as one of the two ways to represent one-to-many relationships, the other being a back-reference (a Project field on the Task itself).

Most Bubble apps end up using both. The Task has a Project field pointing back at its parent (so workflows operating on a single Task can find its Project), and the Project has a tasks list field (so the UI can render “show all tasks for this project” without a search). This sounds redundant, and it is, but the redundancy is what enables Bubble’s editor to auto-generate UI patterns like repeating groups directly off a parent record.

The catch is that Bubble does not maintain both sides automatically. Each side is a separate field, written by separate workflow actions. If you only write one side, the other side stays empty.

Why pointing a child at a parent isn’t enough

A common workflow that produces this bug:

When New Task button is clicked:
  - Create a new Task: Project = Current Project, Title = Input's value

That action creates a Task with Project = SomeProject. From the Task’s perspective, the relationship is correct. The Task knows its Project. A search expression like “Search for Tasks where Project = SomeProject” finds it.

But from the Project’s perspective, nothing has changed. SomeProject’s tasks list field is still whatever it was before. The new Task is not in it. A repeating group bound to “Current Project’s tasks” will not show the new task. A Data API export of SomeProject will return tasks: [] even though the database has the Task.

The fix in the workflow itself is to write both sides:

When New Task button is clicked:
  - Create a new Task: Project = Current Project, Title = Input's value
  - Make changes to Current Project: tasks add Result of Step 1

That works. The bug is what happens when the second action gets skipped, fails silently, or never gets written in the first place. On any project I have inherited where the original developer was new to Bubble, or the work was done by an AI tool that did not understand the two-sided maintenance requirement, the parent-side list fields are partially or fully drifted from reality.

The Finalize Parents pass

The Finalize Parents pass is a workflow that, for each parent record, queries the database for its actual children using a search, then writes the resulting list back to the parent’s list field. The pass is idempotent: every run produces the same state, regardless of the previous state of the list field.

In Bubble’s workflow editor, the pattern is:

Backend workflow: finalize_parent
  Input: parent_id (text)

  Step 1: Search for Children
    Type: Task
    Constraint: Project = (Search for Project where unique_id = parent_id):first item

  Step 2: Make changes to a Project
    Project to change: Search for Project where unique_id = parent_id:first item
    Field: tasks
    Operation: set list (NOT add)
    Value: Result of Step 1

Two notes on the operation. Set list (not “add list”) is required: it overwrites the existing field with the searched result, which is what makes the pass idempotent. The search runs from the child side, treating the back-reference (Task’s Project) as the source of truth and rebuilding the parent-side list from it. This is the structural choice that makes the whole thing work: child-side references are written transactionally with the child record itself, so they are reliable; parent-side list fields are written separately and are not.

To run the pass over the whole database, schedule a parent workflow that paginates over all parents and triggers finalize_parent for each:

Backend workflow: finalize_all_parents
  Step 1: Schedule API workflow on a list
    Workflow: finalize_parent
    List: Search for Projects (paginated)
    Field: parent_id

This is a one-time corrective sweep. On healthy systems, the workflow that creates the child should write both sides explicitly, and the Finalize Parents pass becomes a periodic verification rather than a routine fixer. But for any inherited Bubble app, run it once on import and inspect the diff.

On the migration the silent-failures case study covers, the Finalize Parents pass restored 722 parent-side relationships. Every one of them had been silently empty until the pass ran.

Why this isn’t unique to Bubble.io

Other platforms have variations of the same bug:

  • Airtable. Linked-records fields auto-maintain both sides, which avoids the drift problem in normal use. But under load (a batch import via the API), aggressive list updates can hit rate limits and partially commit, leaving inconsistent state. The fix is the same shape: re-query and reconcile.
  • Notion. Relation properties can be one-way or two-way (configurable). One-way relations have exactly the Bubble.io drift problem. Always use two-way relations on production data unless you have a specific reason not to.
  • Salesforce. Master-detail relationships auto-maintain. Lookup relationships do not. Choose the right relationship type when you design the schema.
  • Postgres / MySQL via an ORM. SQL JOINs always reflect reality, so reading the relationship from a JOIN is always correct. But ORMs that cache the parent-side collection (Rails has_many, SQLAlchemy relationship) can drift if the cache is not invalidated after a child write. The fix is to always reload the parent before depending on its children list.

The platform-specific names differ. The pattern is the same: denormalized relationship fields drift from the source of truth, and reading them without re-verification is reading stale state.

Generalizing: re-query reality

The lesson from the Finalize Parents pattern is bigger than list fields. It is the same lesson that runs through every silent failure I have diagnosed:

Re-query reality. Never trust the platform’s success message that the relationship was written. Verify it exists from both sides before treating the work as done.

This is the corollary to “verify state, not operations” from the silent-failures master post. The platform’s report of what it did is one signal; the database’s actual state is the verdict. Production data work that only reads the report is one bad day away from an audit.

For Bubble.io specifically, the operational habit is:

  • After every batch write, run Finalize Parents on the affected parent type.
  • Build a verification workflow that compares parent-side list-field counts to child-side search counts and flags any mismatch.
  • For new workflows, write both sides of every relationship in the same action group, never separately.
  • For inherited apps, assume the list fields are drifted until a corrective pass proves otherwise.

When to use list fields at all

Given the maintenance burden, when do list fields earn their keep over searches?

Three cases:

  1. Ordering matters and the order is a property of the relationship. A list of comments on a post in chronological order, where reordering is a feature. Bubble’s list field preserves order; a search returns whatever order the database happens to scan in.
  2. Pagination with a fixed page size. List fields give constant-time lookup of “first N items” and “items at index range.” Searches require a sort and a scan.
  3. Data API exports where consumers expect inline children. If your /Project/{id} API endpoint needs to return children inline, the list field is the path; a search would require a separate API call per child relationship.

Outside these cases, prefer searches. They cannot drift. They always reflect the database. The cost is a database lookup, which Bubble handles in single-digit milliseconds for any reasonably-sized table.

Closing rule

If you are running a production Bubble.io app and your downstream automations depend on parent-side list fields, assume those fields are drifted until a Finalize Parents pass proves otherwise. The pass is cheap to run and its result is the ground truth.

If you are running production Bubble.io work across North America, the UK and Ireland, the EU and EEA, or the ANZ region, and you want a database-integrity audit on an inherited app, let’s talk. The Finalize Parents pass is what gets installed on day one.

Frequently asked questions

What is a list field in Bubble.io?

A list field is a field on a Bubble data type whose value is a list of references to other things. A "Project" thing might have a `tasks` field of type "list of Tasks." Bubble exposes list fields in the database editor, makes them queryable from elements like repeating groups, and allows them to be exported via the Data API. Functionally, they look like a one-to-many relationship from the parent's perspective. Mechanically, the list field is stored on the parent row as an array of unique IDs pointing at child rows.

Why doesn't pointing a child at a parent update the parent's list field?

Because Bubble.io maintains the two sides independently. When you create a Task with `Task's Project = SomeProject`, only the Task's Project field is written. SomeProject's `tasks` list field is not automatically updated. The platform exposes the relationship on the child side (`Task's Project`) but not the inverse on the parent side (`Project's tasks`). A workflow that explicitly does `Make changes to SomeProject: tasks add Task` maintains both sides. A workflow that only writes the child side leaves the parent's list empty. This is a Bubble-specific quirk; relational databases auto-maintain the inverse via JOINs, but Bubble's list fields are denormalized arrays.

What is a "Finalize Parents" pass and when do you need one?

A Finalize Parents pass is a second sweep after a data migration or batch write that, for each parent record, queries for its actual children using a search, then writes the resulting list to the parent's list field. You need it whenever the writing workflow only sets the child-to-parent reference and not the parent-to-children list, or when a previous version of the workflow was buggy and left list fields stale. The pass is idempotent: it always overwrites the list with the searched result, so running it twice produces the same state as running it once. On a recent migration of mine, the Finalize Parents pass restored 722 parent links that had been pointing one direction but not the other.

When should I use list fields versus searches in Bubble.io?

Use a search ("Search for Tasks where Project = SomeProject") whenever you can; it always reflects the database state and never goes stale. Use a list field when (1) you need ordered relationships and the order matters (a list of comments on a post in chronological order), (2) you are paginating with a fixed page size and the list field gives you constant lookup time, or (3) you are exporting the parent via the Data API and the consumer expects the children inline. The trade-off is that list fields require explicit maintenance and can drift; searches require a database lookup but are always correct.

Does this list-field issue apply to platforms other than Bubble.io?

Yes, in different forms. Airtable's linked-records field auto-maintains both sides, so it avoids this issue, but Airtable's API rate-limits aggressive list updates and you can hit a different version of the same problem under load. Notion's relation property has a configurable "two-way relation" that you can leave one-way; one-way relations have the same drift property as Bubble's. Salesforce master-detail relationships maintain both sides automatically; lookup relationships do not. The general rule across all of them: if the platform offers two-way relationship maintenance, use it; if it offers only one-way, plan for a finalize-parents pass.

How do I verify that list fields are correctly populated?

Build a verification step that, for each parent, queries for its actual children and compares the count and the IDs against the parent's list-field contents. Any mismatch is a bug. Run this verification after every batch write and treat any non-zero delta as a drift signal. On a production system, this can be a scheduled workflow that reports anomalies via Slack or email. The pattern of "verify state, not operations" applies here directly: the platform's success message that the relationship was written is not the same as the relationship being correctly readable from both sides.