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 this: 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. Meanwhile, “Search for Tasks where Project = SomeProject” returns the correct seven children every time.

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

This post covers the pattern that fixed silent failure #2 in my recent Bubble + n8n migration case study. The fix shape generalizes: every platform with denormalized relationship fields has some version of this bug.

How Bubble.io’s list-field model actually works

List field: a field on a Bubble data type that stores an array of references to other records. A Project type with a tasks field of type “list of Tasks” is the canonical example. Bubble stores the list as an array of unique IDs on the parent row.

Bubble’s documentation describes list fields as one of two ways to represent one-to-many relationships. The other: a back-reference field on the child (a Project field on the Task itself).

Most Bubble apps end up using both. The Task has a Project field pointing at its parent (so workflows on a single Task can find its Project), and the Project has a tasks list field (so the UI can render all tasks without a search). Redundant by design. The redundancy is what lets Bubble’s editor auto-generate repeating groups directly off a parent record.

Bubble does not maintain both sides automatically. Each side is a separate field, written by separate workflow actions. Write one side; the other 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 creates a Task with Project = SomeProject. From the Task’s perspective, the relationship is correct. A search expression finds it.

From the Project’s perspective, nothing changed. SomeProject’s tasks list field is still whatever it was before. A repeating group bound to “Current Project’s tasks” will not show the new Task. A Data API export of SomeProject returns tasks: [].

The correct workflow writes 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

The bug happens when that second action gets skipped, fails silently, or was never written. On every inherited Bubble app where the original developer was new to the platform, or where a no-code tool generated the workflows without understanding two-sided maintenance, the parent-side list fields are partially or fully drifted.

The Finalize Parents pass

Finalize Parents pass: a backend workflow that, for each parent record, queries its actual children by search and writes the result back to the parent’s list field using “set list” (not “add”). Idempotent: every run produces the same state regardless of the list field’s prior contents.

In Bubble’s workflow editor:

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 things matter here. Set list (not “add list”) overwrites the existing field with the searched result. That is what makes the pass idempotent. The search runs from the child side, treating the Task’s Project back-reference as the source of truth. Child-side references are written transactionally with the child record itself. They’re reliable. Parent-side list fields are written separately and are not.

To run it 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

On the migration the silent-failures case study covers, this pass restored 722 parent-side relationships. Every one had been silently empty.

For any inherited Bubble app: run this once on import and inspect the diff. On healthy systems going forward, write both sides of every relationship in the same action group, and the Finalize Parents pass becomes periodic verification rather than routine repair.

Does this apply to other platforms?

The drift problem isn’t Bubble-specific. What differs is where the brittleness sits.

Airtable linked-records fields auto-maintain both sides under normal use. Under load, a batch import via the API can hit rate limits and partially commit, leaving inconsistent state. The reconcile step is the same shape as Finalize Parents.

Notion relation properties are either one-way or two-way, configurable per field. One-way relations have exactly the Bubble drift problem. If you’re building on production data in Notion, always use two-way relations.

Salesforce master-detail relationships auto-maintain. Lookup relationships don’t. The right choice at schema design time avoids the problem entirely.

ORMs on Postgres or MySQL (Rails has_many, SQLAlchemy relationship) can cache the parent-side collection in memory. If a child is written directly and the cache isn’t invalidated, the parent object returns stale data. Always reload the parent before depending on its children list.

The common thread: denormalized relationship fields drift from the source of truth. Reading them without re-verification is reading stale state.

Re-query reality

The Finalize Parents pattern is an instance of the same rule that runs through every silent failure I’ve diagnosed:

Re-query reality. The platform’s success message that the relationship was written is not the same as the relationship being correctly readable from both sides.

This is the corollary to “verify state, not operations” from the silent-failures post. The platform’s report of what it did is one signal; the database’s actual state is the verdict.

For Bubble.io specifically, the operational habits that follow from this:

  • 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 list fields are worth the maintenance cost

Searches (“Search for Tasks where Project = SomeProject”) always reflect the database. They never drift. The cost is a database lookup, which Bubble handles in single-digit milliseconds for any reasonably-sized table.

So when do list fields earn their keep?

Ordering is a first-class feature. A list of comments in chronological order where reordering is a user-facing action. Bubble’s list field preserves order; a search returns whatever order the database scans in.

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.

Data API exports expecting inline children. If your /Project/{id} endpoint needs to return children inline, the list field is the path. A search would require a separate API call per relationship.

Outside those cases, prefer searches.

If you have inherited a Bubble app

Assume its parent-side list fields are drifted. Run a Finalize Parents pass on every type that has a list field, inspect the diff, and build a verification workflow that checks counts on a schedule. The pass is cheap; the audit it enables is not optional if downstream automations depend on that data.

If you want a database-integrity audit on an inherited Bubble app, let’s talk.

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, 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.