Essay · 07·2026 · 9 min

The Intent Model

Write it down at the beginning, before anything can drift.

On any enterprise build I take on now, one file gets written before a line of code: a structured JSON contract that fixes what the app is supposed to do. I call it the intent model. Here’s what it is, why it’s JSON, and why I think it’s the piece that decides whether an AI-assisted build survives a contract with real SLAs behind it.

Forty pages of prose, one typed file

A BRD arrives as prose, and prose can describe a system in enormous detail while committing to nothing. On one vehicle booking system for port logistics, the BRD ran about forty pages: four actor types, thirty-seven business rules, and three state machines (shipment, slot booking, pickup execution) that overlapped in everyone’s head. Hand that document to five people, or five LLM sessions, and you get five different apps, each one defensible against the text.

So before anything gets designed or generated, the document gets compressed into one typed artifact with six sections: actors, entities, journeys, business rules, constraints, and open questions. The name is mine; the shape is stolen from everywhere. DDD’s entities, use-case actors, state machines, requirements engineering. What’s new is that one typed file holds all of them at once, and everything downstream derives from it instead of re-interpreting the BRD.

The compression is where the ambiguity dies, because the format has nowhere to hide a contradiction. The best example from that build: an HBL (House Bill of Lading, the document a shipment travels under) turned out to have two lifecycles running at once. Where the container physically is, on_vessel → at_wharf → in_yard → unpacked → collected. And who is responsible for collecting it, unassigned → assigned → delegated → booked. Both true at the same time for the same HBL. The BRD had been quietly blending the two the whole way through, and untangling them took three meetings with the client. Those meetings were the actual work. The file is just where the result gets to live.

An entity, once it’s in:

{
  "name": "HBL",
  "key_fields": [
    { "name": "hbl_number", "type": "id", "required": true },
    { "name": "customs_status", "type": "enum", "enum_values": ["held", "cleared"] }
  ],
  "lifecycle_states": ["on_vessel", "at_wharf", "in_yard", "unpacked", "collected"],
  "state_machine": {
    "transitions": [
      {
        "from": "unpacked",
        "to": "collected",
        "trigger": "driver_pickup",
        "guard": "customs_status == cleared"
      }
    ]
  }
}

The guard is the load-bearing part. “No collection until customs clears” was a sentence somewhere in the middle of the BRD. Now it’s a named condition on a specific transition. Every screen with a pickup button disables it until customs clears, the generated tests assert exactly that, and no future session has to rediscover the rule by rereading the whole document.

Leonard Shelby had a system

In Memento, Leonard can’t form new memories, so he runs his investigation on Polaroids, notes, and tattoos. The film is about how that system betrays him. He writes everything down after the fact, under stress, with none of the context attached, and by the end he’s editing the notes to steer his future self. His last act in the movie is writing down something he knows is false, because he’d rather have a mission than the truth.

Every LLM session has Leonard’s condition. It wakes up with no memory of the previous session, and it fills every gap you leave with the most statistically ordinary thing that fits. People call this hallucination, but under-specification is the better word: the gaps get filled from training data instead of from your project.

The intent model is the tattoo, with two corrections Leonard never made. The writing happens at the beginning, while the client is still in the room to say “no, delegated and booked are different states,” rather than being reconstructed afterwards from a fading memory of a meeting. And the notes validate: a tattoo can say anything, but an intent file has to pass a schema check, and every change lands in a changelog with a version, an author, and a date. The analogy breaks in one place worth naming. Leonard could never lay out every Polaroid and check them against each other. We do exactly that on every commit.

Why JSON and not a very good document

Half of it is about ambiguity and survival: prose carries its ambiguity all the way to the model, JSON forces me to resolve it first, and models are less likely to “helpfully” rewrite JSON files than markdown, so the contract tends to survive contact. The other half is enums. A prose spec says a conflicting edit “should generally defer to the server.” The JSON version has to pick one of last_write_wins | server_wins | prompt_user | merge, and there is no enum value for “generally.”

For the business rules I borrowed EARS, a notation invented at Rolls-Royce for writing jet engine requirements. Every rule takes one of five shapes:

PatternTemplate
Event-drivenWHEN [event] THE SYSTEM SHALL [behavior]
State-drivenWHILE [state] THE SYSTEM SHALL [behavior]
Unwanted behaviorIF [condition] THEN THE SYSTEM SHALL [behavior]
Optional featureWHERE [feature enabled] THE SYSTEM SHALL [behavior]
UbiquitousTHE SYSTEM SHALL [behavior]

It reads stiff on purpose. A rule that can’t be phrased as one of these is a rule nobody has actually decided yet, and it goes into open_questions[] with an ID and a reason, where it waits for a human answer instead of getting a statistical one.

The unbreakable part

Enterprise SLA-grade is a specific bar, written into the contract. On one enterprise email build, a full mail, calendar, and contacts suite, the obligations include a 99% crash-free rate and a two-hour response window on severity-1 defects. Numbers like that come from refusing to let any failure behavior go unspecified, not from generating happy paths and patching whatever breaks in QA.

So the email app’s intent model carries contracts the booking system’s never needed. Every mutation, and there are 264 of them, declares its optimistic behavior, its rollback behavior, and its conflict policy before any code exists:

{
  "id": "archive_thread",
  "applies_to": "mail_thread",
  "trigger": "swipe_right",
  "optimistic_behavior": {
    "action": "remove_from_active_list",
    "feedback": "trigger_light_haptic_and_show_toast"
  },
  "rollback_behavior": {
    "action": "restore_to_original_index",
    "feedback": "show_error_toast_with_retry"
  },
  "conflict_resolution": "last_write_wins",
  "offline_queueable": true
}

Every screen, all 97 of them, declares an empty state, a loading state, and an error state before it’s allowed to exist, and the offline-capable ones declare a stale state on top (what you see when you’re reading cached mail with no signal). And every requirement from the client’s original list sits in a triage map with a status, including the ones we’re not building. Descoped items stay in the file marked descoped, so a scope audit can prove nothing was dropped silently, and the generation step refuses to build from anything unsigned.

This is what “unbreakable no matter what you throw at it” cashes out to. Swipe to archive in a dead zone with no signal, and the thread comes back at its original index with a retry toast, because that exact behavior was agreed on months before the screen existed. Nobody is being vigilant here. The failure modes were enumerated while everyone was still calm, and the pipeline won’t generate a screen that ignores them.

Same artifact, two opposite jobs

The booking system and the email app use the intent model in nearly opposite ways, which taught me more about the idea than either project alone.

Booking systemEmail app
Main jobGet client and team to agree before dev startsDrive code generation through the whole build
ShapeOne TypeScript file, 157 KBTen JSON files, one per domain, about 2 MB
Carries4 actors, 17 entities, 16 journeys, 37 rules, 50 open questionsAll of that plus 97 screens and 264 mutation contracts
Reviewed byHumans: per-section approve or dispute, client sign-offTooling: schema lint and audit on every commit
After shippingRetires; code becomes the truth, a drift report keeps scoreStays the source of truth; generated files carry provenance headers

On the booking system the model is a consensus instrument. It has its own small review app where stakeholders walk through each section and approve or dispute it, and the effect on the schedule was blunt: roughly three weeks spent getting humans to agree, then about a week and a half of shipping screens. On the email app the model is a machine contract, split into bounded contexts (mail, auth, calendar, and seven more) so a session working on compose loads intent-mail.json and nothing else, which keeps the context lean: the slice you’re touching, never the whole contract. Every generated file opens with a header like // @generated-from: intent-mail.json#archive_thread, so when something looks wrong, the argument happens at the model, and the fix propagates instead of being patched in place.

What the model feeds

Agreement is the model’s first job. Robustness comes from what gets generated off it: Zod schemas for every entity, typed API stubs, SQL migrations, screen scaffolds, TanStack Query hooks (one per declared mutation), a design handover doc filtered to what designers need. XState skeletons for the state machines get generated once and are hand-owned afterwards, because regenerating a state machine a human has been hardening is how you lose the hardening. Even the BRD flipped direction on the booking system: the model generates the document now, not the other way around.

On the booking system the export crosses a repo boundary. The model compiles to a single intent-contract.ts that lands in the portal codebase with a do-not-edit header, and a drift script compares the app’s real types against it, field by field, and writes a report of what’s missing.

The visual half gets the same discipline through design tokens, which the stack note covers in more detail. One theme defines every color, radius, and spacing value. Components consume tokens. Figma receives the same tokens through a plugin and is allowed to recompose and refine, never to mint a new color or component. The flow only goes one way:

TweakCN theme editor
  └─ base.css               every color, radius, spacing token
      ├─ component registry ──→ generated screens
      └─ Figma (same tokens) ──→ visual refinement only

(The email app pushes the token idea further: it’s white-label, so a tenant supplies one accent hex and the theme system expands it into a full ramp at runtime. One authored color, whole product themed.)

Put the three together and each system covers the others’ blind side. The intent model pins down what the app does, the token system fixes what it looks like, and the generation pipeline is the only road from either of them to the code, stamping provenance on everything it touches. A session boxed in on all three sides can still be wrong, but it can’t be quietly wrong, and quiet wrongness is the thing that actually kills enterprise builds.

The doubt

Everything above is scaffolding around two model weaknesses: sessions forget, and gaps get filled with confident averages. Both weaknesses are shrinking with every release. Context windows keep growing, memory keeps improving, and I can see the version of this where a model reads the whole BRD directly, holds all of it, asks its own three meetings’ worth of clarifying questions, and builds the whole app with the logic in its head. No JSON intermediary, no guardrails, no me. If that arrives, a good chunk of this article turns into advice on organizing floppy disks, and I’d rather say so here than pretend the method is eternal.

The part I don’t expect to age out was never for the model. The fifty open questions in the booking system’s file weren’t things an AI failed to understand; they were things no human had decided yet. The sign-off block exists because stakeholders disputed rules and someone had to be on record resolving them. When a client asks in month four whether drivers were ever supposed to see pricing, the answer comes back with a rule ID, a version, and a date. Models are getting better at remembering. Getting five humans to agree on what’s true, and being able to prove it later, was never the model’s job, and writing it down at the beginning is still the only fix I know.

Meanwhile the tooling keeps me honest about my own discipline. While writing this piece I checked the version numbers: the booking system’s intent model is at 0.14.0, the contract exported into the portal came from 0.11.0, and the drift report was last generated against 0.7.1. The system for catching drift has itself drifted. Leonard at least kept his tattoos current.

Published 07·2026 · 2,028 words · 9 min
All notes