Mei runs procurement at an equipment manufacturer with about 700 people. Their platform went live three years ago. Last month she asked me a question that sounded like a two-minute request.
"Sales wants to sort the contract amount from high to low. Right now it sorts like text. Can we just change the field type?"
The field is amount. It was created in the second week of the project, in a workshop room, by an analyst who had a dropdown and a deadline. She picked single-line text, because the number came from a PDF and she was not sure yet whether it needed decimals.
Three years and roughly nine thousand contracts later, that ten-second choice has become a six-week project.
The dropdown is not a label
Renaming a field takes a minute. Moving it on a form takes a minute. Changing its type is a different category of work entirely, and almost nobody knows that at the moment they choose.
In a platform like ours, the type is not metadata that sits above the table. It is the table. Every data table compiles down to a real physical table in PostgreSQL, one column per field, and the field type decides what that column is. Single-line text is a bounded string. Multi-selects and attachments are stored as JSON. There is no neutral layer underneath where a type can be reconsidered later.
So the analyst was not picking a display format. She was writing DDL.
Type decides what the database is allowed to do
The second-order effects show up slowly, in reporting.
Indexes are the clearest case. Ours can be created on ordinary columns, but the documentation is honest about the exceptions: function fields, related-record fields, lookup fields, static text and custom components cannot be indexed. Which means the moment you model something as a lookup instead of a stored value, you have also decided that this column will never be a fast filter.
Nobody thinks about that in a workshop. They think about not typing the same supplier name forty times, and they are right to — denormalization for convenience is usually correct. The convenience is just paid for by whoever, two years later, builds a dashboard filtered on that column and watches it time out.
The type also decides what sorting means. Text sorts lexicographically, so "9" comes after "10", and the amount column becomes useless for the one question finance asks every Monday. A float sorts correctly and rounds incorrectly. A precise decimal sorts and rounds correctly and is the only one of the three that survives a reconciliation.
These are ordinary database distinctions. What is unusual is that in a low-code platform, the person making the decision was never taught them, and the interface does not teach them either.
Type decides how many states the world has
My favourite example is the checkbox.
A boolean has two states. A procurement approval has three: approved, rejected, and nobody has looked at it yet. Model it as a checkbox and the third state does not disappear — it becomes null, which is indistinguishable from "the field was added after this record was created" and from "the form did not require it".
The platform will not save you here either. Generated tables ship with exactly one not-null column, the system id; every business column is nullable by default, because nullability is a form setting rather than a storage decision. So the third state exists physically and is unnameable semantically. Reporting on it becomes a negotiation: does blank mean no, or does blank mean unknown? Two analysts will answer differently, and both will be able to point at real rows.
A single-select with three options costs the same ten seconds and keeps the third state.
The type is a statement about the business. We treat it as a statement about the widget.
Type is a promise to every consumer downstream
The database is only the first reader. By year two, a field has many more.
The WebAPI returns what is stored, so a JSON-encoded multi-select becomes a parsing problem for every integration that touches it. The Excel export renders it one way, the dashboard another, the mobile app a third. The importer expects a template that matches the current type, and every historical spreadsheet your team has been emailing around matches the old one.
Expressions are the quietest and most expensive reader. Formulas reference fields and coerce values, and when the type changes, the coercion changes underneath the formula without the formula changing at all. A comparison that meant one thing when both sides were strings means something else when one side becomes a number. Nothing errors. The report just starts disagreeing with last quarter.
And when the type finally changes, it is not an alter table. It is a data rewrite, plus every expression that touched the field, plus every view and dashboard and export template, plus every integration contract, plus the import templates stored in someone's shared drive. The rewrite cannot even be proven lossless, because the old column had no constraints: it contains values that were never valid for the type you are moving to, and somebody has to decide, row by row, what "N/A", "TBD", "1.2M" and "-" were supposed to mean.
The economics are backwards
Here is what bothers me most.
Adding a field is cheap and removing one is politically expensive. So platforms are optimized, culturally and technically, for addition. Every project accumulates fields. Each one is a type decision made by whoever was closest to the requirement that day, usually under time pressure, usually without knowing what the reports would need.
The decisions that take the least time have the longest half-life. A workflow can be redesigned in a week. A permission model can be tightened in an afternoon. A field type, once nine thousand records depend on it, is a migration project with a steering committee.
We have built an interface where the cheapest action is the most permanent one, and we have not labelled it.
What the platform owes the person at the dropdown
Some of this is unavoidable. Types must be types, and a platform that pretends otherwise is lying.
But there is a difference between a hard decision and an invisible one.
A field type should show what it forecloses at the moment of choosing: whether it can be indexed, how it sorts, how it will appear in exports and in the API, and what changing it later will cost. Not a warning dialog on every field — that becomes noise. A single line, in the same place where you pick the type, saying what you are giving up.
A type change should be an operation with a plan rather than a quiet edit: show every expression, view, export and integration that reads the column; run the conversion in dry mode against real values; tell me how many rows will fail to parse before I click. We do the opposite. We let the change succeed and let the failures surface later, as a report that no longer adds up.
And a platform should treat a small number of fields as architecture rather than content. Amount, date, status, owner, and the identifier that everything joins on. Those five deserve a review before go-live, in the same way a schema review happens in a codebase. Not because they are hard to name, but because they are expensive to be wrong about.
The uncomfortable conclusion
The tempting fix is to make types flexible: store everything as text, parse at the edges, let each application decide what the value means.
That does not remove the decision. It moves it to every consumer, and it removes the one thing a type was giving you — a single, enforced, queryable definition of what the column contains. You get a platform that never blocks a migration because it never knew what the data was.
The real failure is not that the analyst chose text for an amount. Given what she knew in week two, text was defensible.
The failure is that the platform let a ten-second decision stay invisible for three years, and then presented the bill as a six-week project nobody had budgeted for.
We are very good at making the first ten minutes feel effortless.
We have never once shown the customer what the next ten years cost.
Top comments (0)