Beyond the list in Coda
Rethinking Coda’s Data Architecture.

made with Gemini AI
Beyond the list in Coda
Rethinking Coda’s Data Architecture.
Back in 2021, I wrote a foundational blog post with a simple claim: in Coda, everything is a list. If a table has 10 rows, any column has 10 items. Even an empty cell — an IsBlank() — still occupies a position in that list. Every item has a place, and you navigate using that place. Nth(3) gives you item number 3. CurrentValue refers to whichever item you're currently evaluating. Sequence(), [ForEach()](https://coda.io/formulas#ForEach), and Nth() were the tools that made this positional logic work.
It was a powerful mental model. For beginners trying to understand why Coda formulas behave the way they do, it unlocked a lot. Thinking of a column as a list — ordered, traversable, position-aware — made the formula language feel navigable.
But here’s what I didn’t say in 2021, because I hadn’t fully confronted it yet: that mental model has a ceiling. As builds scale into professional, enterprise-grade tools, you hit that ceiling hard. Leaning too heavily on list logic isn’t just limiting — it’s a trap. In this post, we’ll explore how far the list paradigm can take you, where it breaks, and why shifting to “set thinking” is the defining step for any serious Coda builder.
The wall
You’ve probably felt it. A doc that worked beautifully at first starts slowing down as data grows. Formulas that were instant at 200 rows become sluggish at 2,000. Blaming Coda is the natural reaction, but it’s almost always wrong.
The real culprit is list logic baked into the architecture at a level too deep to patch with formula tweaks. And with Coda’s next generation of tables supporting up to one million rows, this problem is about to become impossible to ignore.
Understanding why this happens requires stepping back from Coda for a moment. The problem isn’t new, and neither is the solution.
The LISP trap
In 1958, a programming language called LISP — which literally stands for LISt Processing — revolutionized computing by treating everything as an ordered list. Developed by John McCarthy at MIT as a language for artificial intelligence research, and later documented in his own words, its core idea was elegant: represent everything as an ordered sequence of elements, and process it by walking from one to the next. To find what you need, start at the beginning and check each item one by one. Item 1, check. Item 2, check. Repeat until done.
LISP and early Coda thinking share the same underlying DNA — and the same underlying weakness.
Consider a formula like this, which is more common than you might think:
[DB Tasks].Filter([DB Tasks].Contains(thisRow.[Related Tasks]))
This formula lives in a column. That means Coda runs it once for every row in the table. For each of those rows, it walks the entire [DB Tasks] table to check every item against the current row's related tasks. If you have 10,000 rows, you're not doing 10,000 operations — you're doing 100 million: every row triggers a full walk of every other row. That's O(n²) complexity, and it's why this kind of formula feels fine at 200 rows, sluggish at 2,000, and completely broken at 10,000. Every row added doesn't make the problem a little worse — it makes it exponentially worse.
This is why docs that survive at modest scale will fail at one million rows. The new table capacity isn’t just a storage upgrade. It’s an architectural stress test, and any doc built on this kind of formula pattern will not pass it.
From checking each item to describing what you want
In 1970, Edgar Codd — a British mathematician working at IBM’s San Jose research lab — published a paper called “A Relational Model of Data for Large Shared Data Banks” that would become the foundation for SQL and every modern database system. His insight was deceptively simple: instead of describing how to find your data, describe what the data looks like. Let the system figure out the path.
Before Codd, databases required programmers to navigate data structures manually — specifying physical locations, following pointers, traversing hierarchies. Donald Chamberlin, one of the co-creators of SQL, later recalled that exposure to Codd’s model was “a revelation”: queries that had required complex programs could suddenly be expressed in a few simple lines. The tools that made this possible were straightforward in concept, even if powerful in combination. A WHERE clause lets you describe membership criteria — "give me rows where this condition is true" — without specifying how to find them. A JOIN links two tables through shared values, resolving relationships without manual navigation. An index, maintained by the database engine, means the system can find matching rows directly rather than scanning every row in sequence. Together, these tools shifted the burden of how from the programmer to the engine. You declare the shape of the result; the query planner decides the most efficient path to it.
Think of the difference between a card catalogue and a search engine. A card catalogue requires you to know the system — which drawer, which section, which ordering convention — before you can find anything. A search engine asks only what you want. You describe the result; the system figures out the path. That is the core of what Codd proposed, and it took the industry most of the 1970s and 1980s to fully adopt it.
In Coda terms, the 2021 model is the card catalogue version. You know where items are by position — Nth(3), CurrentValue, the item at this location in this list. Set thinking doesn't ask "where is it?" It asks "what does it look like?" — and lets the engine figure out the rest. Both can produce the same answer. Only one becomes a liability as your data grows.
Shedding the spreadsheet habit
Most people arrive in Coda from Excel or Google Sheets, and bring their layout instincts with them. The 2021 list model quietly reinforces this: if a column is a list and a row is a position, it feels natural to keep adding columns for each new type of data. One column for Revenue, one for Expenses, one row per month. Clean, readable — and in Coda, a liability.
The database way rests on one rule: one row per event, one fact per column. Instead of separate “Revenue” and “Expense” columns, you have a single Amount column and a Transaction Type column. Total profit becomes a single clean formula. The structure doesn't break when the business changes — it just gets a new row.
This means one central DB table holds the raw data. Everything else — forms, charts, kanban boards — is a view on top of it. One source of truth, multiple ways to look at it. Schema first, presentation second.
That discipline is the structural prerequisite for what comes next. Set-based formulas only become powerful once the data they operate on is genuinely relational. A well-structured tall table gives your formulas a clean set to work with. A wide layout gives them a mess to fight through.
Where Coda sits between these two worlds?
So if the relational model is the destination, where does Coda actually sit? Closer than a spreadsheet, further than a database — and understanding that gap is what separates builders who scale from builders who stall.
Coda’s formula engine lives in a hybrid middle ground. [Filter()](https://coda.io/formulas#Filter), [Sort()](https://coda.io/formulas#Sort), and [CountUnique()](https://coda.io/formulas#CountUnique) look set-based on the surface — you're describing what you want, not how to find it. But Coda tables are reactive: every formula column recalculates automatically whenever something changes. That reactivity is what makes Coda feel live and connected. It's also what makes list logic so costly at scale.
The moment a formula in one row needs to look at other rows — which the 2021 pattern of navigating lists by position naturally encourages — Coda evaluates that formula once per row, every time anything updates. [Tasks].Filter(Project = thisRow) on a 3,000-row Projects table means Coda runs that filter 3,000 times — once per project row, on every recalculation. You've built a massive nested loop without writing a single explicit loop. This is the LISP trap dressed up in modern interface design: the CurrentValue-and-Nth() world of 2021, scaled until the wheels come off.
The formula that looked fine at 200 rows isn’t just slower at 2,000 — it’s doing ten times more work. At 20,000 rows, a hundred times more. This is the O(n²) problem expressed not in theory, but in a formula column you’ve probably already written.
What this means for building with AI
There’s one more dimension to this shift that didn’t exist when the 2021 post was written: AI is now not just assisting with Coda formulas — it’s operating inside Coda docs as an agent, executing skills, reading tables, and making decisions based on the data it finds there.
This changes the stakes of architectural clarity considerably — in both directions.
When you ask an AI assistant to write a Coda formula without giving it context, it defaults to the most common patterns in its training data — [Filter()](https://coda.io/formulas#Filter) chains, [Sort()](https://coda.io/formulas#Sort)+[Last()](https://coda.io/formulas#Last) lookups, list-walking logic. Technically correct, architecturally expensive. It's essentially reproducing the 2021 mental model, because that's what most Coda content looks like.
The fix is simple: give it the right inputs. Share your table structure, your relation columns, your naming conventions, and a clear description of the set you’re trying to describe. Show it the [WithName()](https://coda.io/formulas#WithName)+[Max()](https://coda.io/formulas#Max) pattern once with an explanation of why it's better than Sort() + Last(), and it applies that pattern correctly going forward.
The deeper opportunity is architectural. An AI agent operating on a clean relational schema — with consistent DB prefixes, explicit relation columns, and aggregations pre-resolved in summary tables — can reason about the data model, not just react to individual rows. It can identify which table owns a piece of data, navigate relations without being told how, and execute skills that depend on understanding structure rather than just reading values. A tangle of wide tables, ad-hoc filters, and spreadsheet-bias layouts doesn't give an AI agent anything to reason about structurally — it can only patch things formula by formula, row by row.
The four patterns that follow aren’t just performance advice. They’re the foundation that makes serious AI collaboration possible.
What better architecture actually looks like
The shift from list logic to set logic plays out in four practical patterns.
1. Relations are indexes, not shortcuts
A Relation column is Coda’s equivalent of a database index. When a Task is linked to a Project via a Relation, thisRow.[Tasks] on the Project side is a direct lookup — Coda already knows which tasks belong to which project, without walking the list to find out. That's the difference between a direct lookup and a full table scan repeated once per row.
A simple rule of thumb: if you find yourself writing the same [Filter()](https://coda.io/formulas#Filter) in more than three places, or inside a large table's formula column, that's a signal it should be a Relation column instead.
2. Let the schema do the thinking
In a list-logic doc, formulas carry all the intelligence and tables are just containers. In a set-logic doc, the table structure carries the intelligence — the relations, the naming, the organisation — and formulas just display the result. The cleaner your schema, the simpler your formulas — and the less the engine has to recalculate every time something changes.
3. Pre-calculate totals in a summary table
Every formula that aggregates data — totalling transactions, counting completed tasks, summing revenue by category — re-evaluates every time anything in the source table changes. If that formula lives in a canvas or a chart, it runs on every page load. If it lives in a formula column, it runs once per row on every recalculation. Either way, the cost is real and cumulative.
The solution is to resolve these aggregations once in a dedicated [DB Summary] table, and point everything else at the result. The heavy work happens in one place; the rest of the doc just reads a number. As a side effect, it forces you to be deliberate about which aggregations actually matter — a discipline that tends to clarify the whole data model.
A note on views: Coda has them, and they look like the SQL equivalent on the surface. In practice, views on standard Coda tables add reactive overhead rather than reducing it — they’re a presentational layer, not a computation layer. The exception is workspace tables, which are server-side: there, views resolve efficiently and behave much closer to their SQL equivalent. For most builders working in standard docs, the summary table pattern is the right tool.
4. Rethink “preceding value” logic
The first three patterns operate at the schema level — how you structure tables, relations, and aggregations. This fourth one is where the architectural principle meets the formula bar.
The classic approach inherited from 2021 list thinking to finding a previous value: filter the relevant rows, sort them by date, grab the last one — [Sort()](https://coda.io/formulas#Sort).Filter(Date < thisRow.Date).[Last()](https://coda.io/formulas#Last). It works. But sorting is expensive and completely unnecessary here.
You’re not interested in the order of the rows. You want the one with the highest date. That’s an aggregation question, not a list question — and O(n log n) sorting is the wrong tool for it. This is the same family of problem as the O(n²) schema issue: unnecessary work, accumulated row by row, invisible until the dataset grows.
[Max()](https://coda.io/formulas#Max) answers it in O(n) — a single linear scan, no ordering required. Across 500 transaction rows, that's the difference between roughly 93,000 comparison operations and 18,000. The gap widens with every row added.
Combined with [WithName()](https://coda.io/formulas#WithName) — which names a filtered set so you don't calculate it twice — the pattern becomes both faster and easier to read:
WithName(
[DB Exchange Rates].Filter(
[Date] <= thisRow.[The Date] AND
[Target Currency].Contains(thisRow.[Original Currency])
),
MatchingRates,
MatchingRates
.Filter([Date] = MatchingRates.[Date].Max())
.First()
.Rate
)
The filter runs once. Max() finds the highest date in a single scan. The second Filter() isolates that row. Sort() + Last() gets you to the right answer via a side effect of ordering — the 2021 model in formula form. Max() + WithName() gets you there by asking the right question directly.
The same instinct appears in date calculations. To count full months between two dates, the list-thinking approach uses [ForEach()](https://coda.io/formulas#ForEach): loop through a sequence, shift the date forward using [RelativeDate()](https://coda.io/formulas#RelativeDate) for each step, count the successes. Clean to read, expensive to run — eleven date-aware calculations per row, every time the formula evaluates.
The alternative is arithmetic over iteration. Month counts are fundamentally a subtraction problem:
WithName(
(end.Year() - baseDate.Year()) * 12 + end.Month() - baseDate.Month(),
rawMonths,
Max(0,
If(baseDate.RelativeDate(rawMonths) > end,
rawMonths - 1,
rawMonths))
)
Multiplying integers is effectively free. Because month boundaries are uneven, one [RelativeDate()](https://coda.io/formulas#RelativeDate) call verifies and adjusts the estimate where needed. Eleven calculations become one. The formula doesn't loop through possibilities — it calculates the answer directly and checks it once. More details in this blog.
Both patterns make the same point at the formula level: the moment you find yourself iterating through a sequence to arrive at a single value, ask whether that value can be derived directly instead. Usually it can. The loop is a habit, not a requirement.
The question to ask before every formula
Before writing any formula that touches another table, ask one question: am I describing a set, or am I walking a list?
In 2021, walking the list was the lesson. Knowing that a column has items, that each item has a position, that CurrentValue is your handle on whichever item you're evaluating — that was the right foundation for where Coda builders were at the time. This post is the next step.
Walking a list should be a conscious choice, not a reflex. When you catch yourself doing it, check whether a Relation column, a Summary table, or a [Max()](https://coda.io/formulas#Max) reframe would do the job instead. Sometimes it's the only option. But it should always be chosen deliberately, with eyes open to the cost.
The economic logic is straightforward. When a doc is small, every approach feels fine — computation is effectively free, everything responds instantly, architectural decisions have no visible cost. As the doc grows, that changes. The accumulated cost of list-logic decisions, each one reasonable in isolation, becomes the dominant constraint.
That threshold just moved. Coda’s new workspace tables are server-side, which means they can hold up to one million rows and resolve queries with the kind of efficiency that standard client-side tables cannot match. That’s genuinely exciting — but it makes architectural discipline more important, not less. A list-logic formula that struggles at 10,000 rows doesn’t become viable at 1,000,000 because the table moved to a server. The O(n²) cost scales with the data, wherever that data lives. The builders who will actually benefit from that capacity are the ones who stopped walking lists before they needed to.
The shift to set thinking isn’t about writing more sophisticated formulas. It’s about asking better questions before the first row is entered — what relations does this data have, where does aggregation belong, what should the schema know so the formulas don’t have to?
The list was where we started. The set is where we’re going.
On a personal note, creating these in-depth posts takes a lot of time and effort. While I love sharing my knowledge, a little support goes a long way. If you found this helpful, what about a donation or sharing this post with your fellow Coda enthusiasts? Every bit of encouragement helps fuel the next deep-dives!
My name is Christiaan, and I regularly blog about Coda. If you’d like to take this further with hands-on support for your own Coda setup, I offer professional consultations — feel free to reach out. You can also find my free contributions in the Coda Community and on X. The Coda Community is a fantastic resource for free insights, especially when you share a sample doc.
메타데이터
- post_id
- f4bdf226f896
- slug
- beyond-the-list-in-coda-f4bdf226f896
- url
- https://medium.com/@huizer/beyond-the-list-in-coda-f4bdf226f896
- canonical_url
- https://medium.com/@huizer/beyond-the-list-in-coda-f4bdf226f896
- author_url
- https://medium.com/@huizer
- status
- ok
- fetched_at
- 2026-06-20 20:29:01