Lin runs supply chain at a 400-person manufacturer. Eight months after go-live, she sent me a screenshot: 1,914 purchase orders, every one of them with an empty supplier field.
The supplier field is required. It has been required since the first week of the project.
Her message was one line long. "The form will not let me save this. So who saved it?"
I knew the answer before I opened the file. The platform saved it.
The form is one door
When people describe validation in a low-code platform, they describe the form. The official feature list does the same thing: data validation is listed inside the Form Designer, next to conditional display and dynamic calculation. That is where it is documented, and that is where almost every builder puts it.
It is also, statistically, the least used way data gets into a table.
Count the doors on a platform that has been running for a year: the form, the Excel importer, the public WebAPI, a scheduled script, a workflow transition that updates related records, an inbound webhook from the ERP, an AI agent that adds and edits records, and the CSV somebody's intern pasted in during the migration weekend.
Eight doors. One lock. And the lock is on the door that requires a human to be sitting at a desk.
Validation on the import path is a checkbox
This is the part that surprises people. The importer has validation too — but it is off until somebody turns it on.
Open the import control configuration and you get an option to enable data validation for imported rows, then a validation expression you write yourself, then a failure message, then two more switches: whether to keep importing rows that fail, and whether to skip the current row instead. The default posture is permissive. The expression is authored by whoever configured that particular button, on that particular day, for that particular table.
So the same supplier field the form refuses can be written 1,914 times by an importer nobody remembers configuring. The rule now exists in two places, with two authors and no shared source of truth. Your form says the supplier is mandatory. Your import button says the supplier is mandatory if someone ticked a box three years ago.
The interesting question is not why the data got in. It is which of the two is the real rule.
The database does not agree with either of them
Most platforms could enforce more than they do. Ours generates a real physical table per data table, DDL and all. The platform owns every column. It could push constraints down.
It mostly does not. Read the storage documentation and you will find that the generated table has exactly one not-null column: the system id. Every business column ships nullable, because nullability is a form setting, not a storage decision. The one clear exception is uniqueness, and uniqueness genuinely lives at the database layer, implemented as an index. The docs are explicit that a violation is rejected on create or on update.
That asymmetry is worth sitting with. The one rule that lives below the application is the one rule the application cannot fake. Everything else — required, range, format, date window — lives above the database, inside a layer that only some writes pass through.
The rules are moving too
Lin's 1,914 rows were not created under the current rules. They were created under whatever rules existed the last time each row was touched.
Required is a switch on the field. Conditional-required is an expression somebody wrote. Format checking exists on single-line text — email, URL, phone number, ID number — but not on the field type next to it. Integer ranges apply only if someone enabled the range option. Uniqueness needs an index that someone had to create deliberately. Date restrictions are configured field by field.
None of this is wrong. Each one is a reasonable product decision made by a reasonable person at a reasonable moment.
The consequence is that your rule set has a version history and your data does not. A row created in March was validated against March's rules. Tighten a rule in June and you have silently split one table into "born valid" and "born invalid" — and no column tells you which is which. You find out eight months later, from a human, in a screenshot.
Scripts, workflows, and agents enforce intent, not shape
Then there is the layer that writes data on purpose instead of by hand. The script API exposes direct record updates and batch updates, and the documentation does not promise that form rules run. Some of those calls carry explicit switches for whether to write a change entry and whether to recompute rollup fields. Those switches exist because someone needed a write to be faster, quieter, or both.
Permissions do not close this gap. When an AI agent edits records it acts with the permissions of the calling user. That answers who is allowed to write. It says nothing about whether what was written is valid. Authorization and validity are different questions, and low-code platforms are excellent at the first and informal about the second.
Write paths multiply because the platform is successful. Every integration, every automation, every agent is another door — and each new door is added by someone who has never opened the field's validation settings.
Nobody owns the repair
Here is where the problem actually lives, and it is not a validation problem.
When dirty data appears, three fixes are available: fix the data, fix the rule, or fix the door. Choosing between them requires knowing who owns the rule and what the rule promises. Most platforms can tell you who changed a record — and often only if change tracking was enabled on that table in the first place. Very few can tell you who owns a rule, when it took effect, or what it replaced.
The platform's own data modeling guidance hints at this. It says every important field needs a definition, a type, a required rule, an accepted range, and an owner — and then, in the FAQ, that the model should be reviewed "when data-quality issues reveal unclear ownership or definitions." That is the vendor admitting the real signal is ownership, not coverage.
The uncomfortable conclusion
The instinct is to add more validation. It is the wrong instinct, and it makes things worse.
Every rule you add to the form widens the gap between the doors, because you only ever add it to the door you use. The importer, the script, the webhook and the agent keep writing under their own rules, which are now measurably older than yours. You will have made the form stricter and the table no cleaner.
A platform cannot make bad data impossible by adding a checkbox to a screen. What it can do is make bad data visible and repairable: treat a rule as an entity with an owner, a version and a scope; apply it at every write path instead of the polite one; and make it possible to re-run that rule over history, so "born invalid" is a query rather than a discovery.
You did not have a data quality problem.
You had eight front doors and one lock.
Top comments (0)