Requirements as Data
A parts list is inspected before it reaches the line; your specification deserves the same, and a paragraph can never be inspected that way.
TL;DR. Prose is the wrong container for a requirement, and it fails slowly, silently, and totally: the sentence that is secretly three requirements, the dependency on a feature you quietly cut, the owner who evaporates the moment the document is shared. Stop writing prose for this one job and write each requirement as a structured record against a schema, an id, an owner whose phone rings, explicit acceptance criteria, and its dependencies, then run a check on every change. You get goods-in inspection for intent, a bill of materials for the thing you are building, where malformed rows are rejected at the door instead of surfacing late, expensive, and personal, out on the line. It is the cheapest layer, it pays in week one, it needs no Lean, and it is the floor every harder layer stands on.
Listen to this essay (17 min)
Narrated by Charlie via ElevenLabs
Open any requirements document more than a few months old and read it slowly. Somewhere is a sentence that begins "the system should" and then describes three different things joined by "and". Somewhere else a line leans on a feature the team quietly cut in the second sprint, so it still implies work that can never be done. Somewhere a paragraph and a backlog item say almost the same thing in the same words, and no one can tell you which is canonical [the single authoritative version], because both look equally official. You read the whole thing and still cannot answer the one question that matters when something breaks: when this requirement fails, whose phone rings?
This is not a discipline problem. The people who wrote that document were careful. It is a format problem. Prose is the wrong container for a requirement, and it fails in a way that is slow, silent, and total. The fix is not to write better prose. It is to stop writing prose at all for this one job, and to write each requirement as a structured record you can inspect like a parts list at goods-in, where structure catches the defects prose hides.
A requirement is a part, not a paragraph
Think about what a real build runs on. Not a memo that says "the bicycle should have two wheels, a comfortable seat, and good brakes," but a bill of materials [BOM: the structured parts list a build runs from, where every component carries a part number, a quantity, a named supplier, and a parent assembly]. Every part has a number. Every part has a supplier who is responsible for it. Every part rolls up into a sub-assembly, which rolls up into the finished product, so you can trace any part number up to the thing it serves and down to the parts it needs. Nothing in a BOM is described in a sentence. Everything is a row, and every row has the same columns.
The reason a build runs from a BOM and not a memo is not bureaucracy. It is that a memo cannot be checked, and a BOM can. You can run a malformed BOM into goods-in [the inspection step where incoming parts are verified before they enter the line] and the inspection rejects it: a part with no supplier, a quantity of zero, a component discontinued two revisions ago. The structure is what makes the error catchable before it reaches the line. A memo with the same errors sails straight through, because there is nothing to check it against.
Software requirements are still written as memos. We have version control, continuous integration, and infrastructure that builds itself from a file, and at the top of that stack, where intent enters, a paragraph. The highest-leverage move for most teams is not a new framework or a faster pipeline. It is to give the requirement the same treatment the humblest bracket in a bicycle already gets: a part number, a supplier, and a place in the assembly. Write it as a record against a schema [a formal definition of what fields a record must have and what shape each field takes] and stand up a small check that runs on every change, and you get goods-in for the record that carries intent, checked as it enters.
Three fields carry most of what follows:
{ "id": "REQ-014", "owner": "alice-chen", "depends_on": ["REQ-002"] }
an id that names the requirement, an owner whose phone rings, and a depends_on that says what it needs. The next section is what goes wrong when a memo leaves them implicit.
Four failures: two stopped, two surfaced
A schema does not make your requirements correct. Paired with a standardiser [a small tool that loads the file and checks every record against the schema], it acts on four defects that prose hides so well you cannot count them. Two it stops: a requirement with no owner is blocked at the ready-gate, since a proposed record may legitimately still lack one, and a dependency on something deleted is rejected outright. The other two are well-formed on their own, so it cannot reject them; instead it drags them into the open, where they become visible, countable, and assignable.
The part with no supplier: a requirement with no owner. In a memo, ownership is implied by proximity, the writer of the section or the team at the top, and evaporates the moment the document is shared. A schema makes owner a required field, so a requirement with no responsible human cannot move past proposed to ready. This is the single most valuable constraint in the format: "whose phone rings?" now has an answer forced at the moment of writing, not reconstructed in a crisis. A team label is not an owner: "the platform team" resolves to no one at two in the morning. The field must resolve to a single accountable person, named directly or through a role or on-call rotation that dereferences [resolves through one more step] to one.
The part number pointing at a discontinued component: a dependency on something deleted. A memo can happily say "this builds on the search feature" long after search was cut, because nothing connects the words to reality. A schema makes depends_on a list of identifiers, and the standardiser confirms, mechanically, that every one actually exists. A dangling dependency [a reference to a requirement that is no longer present] becomes a validation error, not a discovery made three weeks into a doomed build.
The line item that is secretly three parts: an acceptance criterion that is really three. "The user can share a note and get a notification and revoke access" reads as one requirement and is three, with three ways to be done and three to fail. In prose this compression is invisible, and it detonates later when one of the three slips and the requirement is marked complete anyway. A schema cannot reject it, because the sentence is well-formed; it makes acceptance a list, which invites you to split at the word "and", and the moment you do, the hidden parts become countable, testable, and individually ownable.
The same bracket ordered twice under two part numbers: silent duplication. Two well-meaning people describe the same need in slightly different words, and now you have two requirements that get built twice, tested twice, and contradict each other the moment someone edits only one. Prose cannot see this, because near-duplicate paragraphs look like two distinct thoughts. The standardiser cannot be sure either, but it can sort records so near-identical titles sit adjacent and flag exact-title collisions outright, turning an invisible problem into a short list a human merges.
None of these four is exotic; every one is in the wreckage of projects you have watched go sideways. Half the value is the rejection, half the refusal to let a defect stay invisible. That is layer one.
What the record looks like
Here is one requirement for an ordinary notes app, written as a record. It is fully invented; the thesis shows a trimmed fragment, here it is whole.
{
"id": "REQ-014",
"title": "A user can share a note with another user",
"owner": "alice-chen",
"status": "proposed",
"priority": 23,
"acceptance": [
"Given a note I own, I can grant another user read access",
"The recipient sees the note in their list within one sync",
"I revoke access and the note leaves their list within one sync"
],
"depends_on": ["REQ-002"]
}
Here is the other half, the thing that does the inspecting, an illustrative ready-gate schema trimmed to the fields discussed here. Its required list is the ready-gate's: a record still proposed is checked against a laxer schema, not shown, that demands only id and status, and owner and acceptance join required only on promotion to ready.
{
"type": "object",
"required": ["id", "owner", "status", "acceptance"],
"properties": {
"title": { "type": "string", "minLength": 1 },
"owner": { "type": "string", "minLength": 1 },
"status": { "enum": ["proposed", "ready", "done"] },
"priority": { "type": "integer" },
"acceptance": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
"depends_on": { "type": "array", "items": { "type": "string" } }
}
}
Read what the format bought. The id makes the requirement addressable, so a test, a commit, or a later formal model can point at exactly this requirement. The owner answers the phone-rings question before the requirement is even agreed. The priority is a number, here a weighted score such as WSJF [weighted shortest job first: ranks the most valuable work that takes the least time], so sequencing is a sort rather than a fresh argument, though only if the score's inputs are written down; an undocumented 23 only hides the argument, it does not remove it. The acceptance is a list of three, so "done" now means all three pass, and depends_on names REQ-002, which the standardiser confirms is real.
Standardising the file is the second half of layer one and the part teams skip. The smallest version that works on Monday is four things: one requirements file under version control, a JSON Schema [a machine-readable definition of the required fields and their types] to gauge each record's shape, a small standardiser to run the checks no schema can express, and a required check on the merge path so a malformed record cannot reach main. The schema is goods-in's gauge; the standardiser is the inspector who reads it, rejecting anything missing a required field, flagging every depends_on that points at nothing or loops back on itself, sorting by priority, and listing likely duplicates to merge. A local commit hook can give the same feedback sooner, but only as advice: one flag skips it and a fresh clone lacks it, so the required merge-path check is the enforcement point. A schema forces owner present and non-empty but cannot tell a person from a team, so the standardiser checks each owner against a directory you already maintain, not a new list to tend.
At the ready-gate a named authority can still promote past a failure, but only as a committed, attributable, time-boxed exception, and a climbing override rate is itself a signal the gate is mis-scoped, not a steady state. One person per requirements file, a tech lead or head of technology, owns the schema, keeps the check running, and adjudicates the duplicate-merge calls, so the discipline is never left ownerless; at multi-team scale each file has its own such steward. The file stops being a document you trust and becomes one a machine inspects: the difference between hoping the BOM is right and knowing the malformed rows were rejected at goods-in.
Why this is the floor the harder layers stand on
A schema checks the shape of each record. It is silent on whether two well-formed requirements contradict each other, a harder question it structurally cannot answer and the subject of the deeper essays here: a formal model must point at something, and a record with a stable id is the handle it attaches a checked definition to. You cannot model what you have not first named and located, which is why structured requirements come first, even if you never make the second move.
The objection that lands hardest
The strongest objection is not that this is too much process. It is that you already have it: my issue tracker stores tickets with owners, fields, and links, so this is a rename of something I bought years ago. It is half right, and the half that is wrong is the whole point.
A tracker stores the data. It does not enforce a schema on the content, and that gap is everything. In a typical tracker the owner field can be blank, the acceptance box can hold a wall of prose or nothing, a dependency link to a deleted ticket just dangles, and the ticket saves anyway. There is no goods-in, because the tracker optimises workflow state, not the well-formedness of the requirement on it. The data is present and unvalidated, the exact failure a BOM exists to prevent. You can run this discipline inside your existing tracker, and many teams should, but the benefit only comes when something rejects the malformed record, which a tracker almost never does. The schema is the part you are missing, not the storage. So let the two divide the labour with one writer per fact: the schema-validated record is the single source of truth for owner, depends_on, and acceptance, and the tracker mirrors them read-only through the shared id, keeping only board-position. That settles what 'done' means; 'consistent' belongs to the formal layer above, since a schema cannot catch two well-formed records contradicting each other. Enforcing the schema then means exporting the records to a CI check or wiring a tracker automation rule, since the tracker will not; that one-way mirror is the steward's job, not free, and the committed ready-gate overrides double as its audit trail.
There is a softer version too: that turning requirements into records makes them rigid, and real product work is too fluid for a fixed form. The opposite is true. A loose paragraph is the rigid thing, because to change it safely you must re-read everything around it and hope you caught the ripples. A record with explicit dependencies tells you what it touches, so you can change it and see, mechanically, what else now needs a look. Structure does not freeze the work. It makes the work safe to move.
Concede the limit plainly: these checks test form, not quality. A schema-valid record can still be vague or wrong; the correctness of intent stays a human judgement, and the structure does not remove it, only makes it cheaper and more localised, one well-formed row at a time.
What exists today, plainly
In the interest of not selling a rounder story than is true: the structured-requirements layer is the mature, boring, shipped part of this programme. It has run across real projects for months, it is the cheapest layer by a wide margin, and it pays in the first week: once the check runs on every change, the first two failures stop reaching the build and the other two stop hiding. It rolls out brownfield [onto an existing codebase, not a clean slate] without a migration: apply the schema to new and edited requirements at the ready-gate, and leave legacy prose until something next touches it; a later tightening of the schema lands the same way, at the next touch, not retroactively. The standardiser is a small tool, not a product, and its duplicate-detection is deliberately crude and partly manual. The formal layer above is younger and harder, the subject of the companion essays; this layer asks for none of it. Do only the thing in this essay and never read another word of the series, and you will already have removed a class of defect that surfaces late, costs real money, and lands on a named person.
Back to the parts list
A build that runs from a bill of materials is not more bureaucratic than one that runs from a memo. It is calmer. The errors that would have surfaced on the line, late, expensive, and personal, surface at goods-in instead, early and cheap, against a row in a file rather than a person in a meeting. The product specification is the last place we still send a memo to goods-in and act surprised when the wrong thing gets built. Give the requirement a part number, a supplier, and a place in the assembly, and you have not added process. You have given the work something it can finally inspect.
This essay is layer one of the "Verified Specification as Code" series. The thesis, "Verified Specification as Code", argues that the product specification is the last place we still reconcile by hand. "When the Spec Can Contradict Itself" is layer two, the contradiction-finding machinery a schema cannot reach. "Continuous Enforcement and Continuous Verification" is the runtime counterpart, checking the records on every change rather than when someone remembers. "When the Document Leaves the Factory" is the two-reader design for stakeholder-facing output, and "Holding Water with Our Hands" applies the same reconciliation-loop argument to statutory compliance.
About the author: Eduardo Aguilar Pelaez is CTO and co-founder at Legal Engine Ltd. He writes on formal methods, AI agents, and the discipline of building systems that survive being walked away from.