What it costs you
Every framework is a shape, and a shape is mostly refusals. The rest of this site is about what Loom lets you do. This page is the other column, and it is the one to read before you commit anything to it.
The bargain, said once more
Loom lets an AI change a page that is already running, and the reason that is not alarming is that it can only say a small number of things. The page is data. A change is data. The words a change may use are a list you wrote.
You give up the freedom to have the AI produce anything at all, and what you get back is a change you can read, measure, refuse and undo. The other half of the same trade: what a thing does lives in the component you registered, never in the tree — so a proposal can move your checkout, rename it and take it away, and it can never change what happens when somebody presses the button.
Everything below is that trade, itemized.
Eight things you can no longer do
Under each one is the ruling that made it so, and what that ruling turned down instead. Those lists are read out of the records as this page builds — they are not a summary of them, and they never say why. The why is in the record, one click away, written on the day. Where a ruling has since been replaced in part, the box says so, because part of it is somebody else's now.
Loom will not write your components
The thing most AI tools do — hand you a new component, freshly written — is the one thing Loom does not do. It rearranges and reconfigures the pieces you already built. If a page needs a seating plan and nobody has ever built a seating plan, no amount of asking will produce one.
What you do instead. You build it once, in the ordinary way, and register it. After that it is a word anybody can use, for ever. This is the trade the whole framework is: generated code is code nobody read, and what you get for giving it up is a change small enough to review in a few seconds.
The ruling
The tree and the delta are the unit of AI-authored change
Turned down with it
- Generated component code.
- The Hermes beta's flat warp.blocks IR.
- A fragment node kind, and conditional/looping nodes.
- Positional paths as the addressing scheme.
A change can only use words that are already yours
Every name a change is allowed to say comes off a list you wrote — your heading, your card, your checkout. Asking for something that is not on it does not produce a broken page later; it produces a change that could not be written down in the first place.
What you do instead. Add the word. Registering a primitive is a deploy rather than a conversation, so your vocabulary grows at the speed of your release process — which is the speed you already trust. Primitives and the registry shows it happening.
The ruling
The registry is what the model is told it may build
Turned down with it
- Put the catalogue in the system prompt.
- Generate a JSON Schema per primitive and constrain the reply to it. (not closed)
- Send the Zod schemas as text.
- Let the interpreter hold the registry directly instead of a projection.
- Derive the catalogue from the tree instead (what types appear in it).
Your component cannot pass a change's settings straight through
A primitive is handed its settings as one object, and it has to read them by name. The familiar one-line shortcut — take everything you were given and spread it onto the element you return — is not available here, because the thing being spread was written by a model and the element it lands on is real HTML.
What you do instead. Read the props you declared, and pass on only what you meant to. It is a few more lines in a file you write once, and it is what stops a proposal reaching an attribute nobody designed for it.
The ruling
Primitives receive AI-authored props in a bag, never spread
Turned down with it
- Spread props and blocklist the dangerous names.
- Spread props and require primitives not to forward rest props.
- Namespace the runtime's props instead (__loomNodeId) and spread the rest.
- Validate props against the primitive's schema in the Gate instead. (not closed)
A word your registry has forgotten leaves a gap, and nothing crashes
Render a page that names a primitive you did not register and the page still renders. The unknown node is left out and a note is filed saying which one and where. Nothing throws, so nothing wakes anybody up — a page missing a whole section can look completely fine.
What you do instead. Read what a render hands back. Every render returns its notes beside the output, and a deployment that ignores them is choosing to find out from a reader instead. Rendering a tree shows it happening.
The ruling
The renderer is a total, pure projection of the tree
Turned down with it
- Return Result<ReactNode, RenderError> and fail the whole render on an unknown primitive.
- Render a visible placeholder for an unknown primitive.
- Promote an unknown primitive's children into its parent.
- Position paths as React keys instead of node ids.
- Let the renderer load the tree itself (a default source, a fetch, a cache).
- Trust the tree coming out of storage and skip parseTree.
Loom has no idea which parts of your page matter
Rewording a heading and changing a price are the same move: one setting, on one node. Nothing in the runtime has heard of your shop, so out of the box it cannot tell those apart, and nothing on your page is protected until you say so.
What you do instead. Write the policy. It is the second list you write after the registry, it is usually a few lines long, and it is what turns “an AI can change this page” into “an AI can change these parts of this page”. What AI may change shows it happening.
The ruling
The Gate is a pure function of two independent axes
Turned down with it
- A weighted risk score with a threshold.
- One combined "risk" axis.
- Reversibility as a declared property of a primitive.
- Snapshot-based undo.
A change is written against one version of the page, and the page will not wait
Somebody asks for a change at ten past nine. Between then and the moment it is applied, two other changes land. The first one does not quietly re-aim itself at the page as it now stands — it names the version it was written against, and when that version has moved on, it does not apply.
What you do instead. Ask again. That sounds like a cost and is mostly a refusal to guess: a change that re-targets itself is a change nobody reviewed, because the thing that was reviewed is not the thing that would happen. When something looks wrong shows it happening.
The ruling
AI drafts a change; the runtime names what it creates
Turned down with it
- The model emits complete LoomNodes with ids.
- The model emits placeholder handles ($0, $1) that the runtime resolves.
- Accept model-supplied ids but rewrite them before applying.
- Author the delta against the tree's current revision rather than the intent's.
There is no rewind
Undo does not wind the page back to how it was. It works out the change that would return things to how they were, and then that change goes through everything any other change goes through — including being refused, if it would throw away work somebody has done since.
What you do instead. Nothing, usually: an undo is a button like any other, and the difference only shows when it is refused. What you get is a history that only ever grows, so the page can always say who did what and when. The history of a page shows it happening.
The ruling
An undo is a proposal, not a rewind — partly superseded
Turned down with it
- Store the inverse delta on the revision.
- Rewind the log.
- Apply the inverse straight to the store, skipping the Gate.
- Let a contested revert proceed with a warning. (not closed)
- A revert operation in the delta model.
You cannot fix a live page by editing the database
The page you can see is worked out from the list of changes that produced it. Reach past that and correct the stored page directly — one quick statement in a database console, the way anybody would — and the page will look right and stop matching its own history. Loom has a check that finds this, and finding it is all it can do.
What you do instead. Propose the correction, even from a script. There is one way in and it is the same one a person uses, so the fix arrives as a change with a name on it rather than as a mystery six weeks later. What your app has to do shows it happening.
The ruling
The log is the truth and the snapshot is a materialised view
Turned down with it
- Pure event sourcing, no snapshot.
- Snapshot only, no log.
- Snapshot plus a log kept only for display.
- Periodic snapshots every N revisions, replaying the remainder. (not closed)
- Deriving the audit from inverse deltas
What is not settled
The eight above will read the same next year. Some of what Loom has turned down will not, because a record sometimes declines to close a question rather than answering it — deferred, rejected for now, worth revisiting once. Those are the constraints that might lift, and there is no reason to make you dig for them.
1023 alternatives, across 216 rulings. Every one of them is an approach somebody could reasonably have taken, written down on the day it was turned down. 40 of them are not closed:
Tool use instead of structured output.
“Worth revisiting if a repair loop needs multiple turns.”
Record and replay real API responses (VCR-style cassettes).
“Not chosen for now.”
Have a second model grade the first one's confidence.
“Rejected for now: it replaces one self-graded number with another, at double the cost, and it is not obviously better than measuring outcomes.”
Confidence is self-graded, trusted on purpose, and must be calibrated
Validate props against the primitive's schema in the Gate instead.
“Rejected for now, and worth revisiting when §4 exists.”
Generate a JSON Schema per primitive and constrain the reply to it.
“Rejected for now, and the most interesting rejection.”
Split into several narrower schemas, one per operation kind, chosen by a first cheap classifying call.
“Worth revisiting rather than dismissing.”
Periodic snapshots every N revisions, replaying the remainder.
“Worth revisiting if trees turn out to be large or edits very frequent.”
The log is the truth and the snapshot is a materialised view
Requiring an append to carry proof of a Gate disposition
“Deferred rather than rejected: it changes the TreeStore contract that landed hours ago, and the right time to change a contract is when a second implementation exists to check the change against.”
Neon.
“Worth revisiting if preview isolation starts to matter, and the swap is cheap precisely because this implementation is plain Postgres.”
An identity provider — Supabase Auth, Auth.js, OIDC.
“Rejected for now, not on principle.”
Let a contested revert proceed with a warning.
“Rejected for now: the Gate cannot see the difference between a clean undo and a partial rollback that silently drops later work — both are just deltas — so a contested revert that the Gate auto-applies would be discarded work with nobody in the loop.”
Mint the policy id, or hash the policy's contents.
“Rejected for now.”
Compact rather than delete — fold old episodes into a summary row and keep that.
“Rejected for now, and it is the most interesting alternative.”
A large-relocation stake factor with its own threshold.
“Symmetry with large-removal, and rejected for now: a relocation is fully reversible and destroys nothing, so it is not damage in the sense stakes measure.”
A move relocates a subtree, and the analysis measures the subtree, not the phrasing
@testing-library/jest-dom matchers.
“Not now; not a difficult decision to revisit.”
Segment the unjudged too.
“Rejected for now.”
A new apps/demo (or apps/site) app.
“Rejected for now on one fact: Vercel builds the project it was configured with, so the demo would have shipped invisible, and "the maintainer has not yet been able to see any of this working" is the specific problem §4b step 4 exists to solve.”
The demo is public, and shares nothing with the portal but the deployment
ICU message format, with interpolation and plurals.
“Refused for now because it puts a formatting language inside the seam, and because nothing in the library needs it yet: the strings that exist are fixed accessible names.”
A render diagnostic instead.
“It remains worth having for trees that arrive from outside the Gate — hand-authored, imported, or written before a host declared its targets — and is left open rather than built.”
A primitive says whether it is a target, and the Gate derives the nesting
Emit a RenderOutput diagnostic in the shape data-unavailable uses (0058), so the page renders and the surface reviewing it is told.
“The more interesting option, and deferred rather than rejected.”
Container queries instead of a width query.
“Correct in principle and the thing to revisit.”
A layout only CSS can express belongs in the stylesheet, width query and all
Summarise the omitted siblings — types and counts by type.
“Attractive and deferred.”
Leave 0079 alone and file the finding a fourth time.
“Rejected as the recommendation because 0079 does not forbid the change — it asks for it, in the words *worth revisiting* — and because the thing being protected is a measured defect on the deployed front door rather than a preference.”
display: contents on the wrapper.
“Worth revisiting the first time a lane files that the box is in its way.”
Part of a tree is rendered by the seam, and the seam is what mounts the theme
A new scrim slot pair in the vocabulary.
“Rejected for now because seventeen slots became nineteen for every palette in existence, including every host's, to express something that is derivable from two slots they have already declared.”
What a palette cannot say about itself is measured from it, never declared on it
One file per finding, findings/YYYY-MM-DD-slug.md.
“Rejected for now rather than on the merits — it moves 368 entries, needs a generated index to stay readable, and it changes a file every routine's brief names by path, which is a coordination cost across seven lanes for a problem one committed line solves today.”
A shared ledger is union-merged, and a generated file is regenerated
A timed-out member on each failure union.
“Rejected for now on the cost of growing a closed set the surfaces walk: two Record maps in (docs) would go red, in another lane's file, to distinguish two states with the same remedy and the same actor.”
A call into foreign code has a ceiling, and the runtime owns it
A session id scoped to the tab, surviving navigation.
“Rejected for now: it starts to resemble identity as soon as it spans pages, and the same funnel question can be asked per view.”
A reader signal stays anonymous, and a funnel is correlated inside one page view
A probabilistic sketch — HyperLogLog or similar — stored per node.
“Rejected for now on two grounds: a sketch of view keys is still a structure derived from them and kept indefinitely, which needs its own argument rather than an inherited one; and it is a real amount of machinery for an error a deployment can already shrink by setting one number.”
A rollup is added to what is stored, and a distinct-view count is therefore approximate
A durable watermark: record how far rollup has got, count promptly, forget on the window.
“Rejected for now, and it is the door to open if the portal finds the lag unacceptable.”
Counting a window of reader signals and forgetting it are one operation
A within scope on FunnelEnd
“Rejected for now.”
A budget a host passes in, checked by the runtime — catalogueFits(cost, budget).
“Rejected for now as a predicate with nothing in it: it is cost.characters > budget, written on the caller's side of a comparison the caller already has both sides of.”
A library is a set to choose from, and a vocabulary is priced per entry
Let a part opt out of PAGE_SEQUENCE, so the vocabulary can grow without lengthening the document.
“Genuinely tempting, and declined for now rather than refuted.”
A page part is earned by the region it occupies, not by the content it holds
A loom.bound wrapper with ready / empty / unavailable regions, beside an ordinary container.
“Worth revisiting if a third bound primitive arrives and the seam grows a way for a child to read its parent's answer — which is a framework question, not this one.”
Open a second PAGE_SEQUENCE for a non-developer page.
“Not refused, deferred with its trigger named: a second sequence is earned by a page whose *regions come in a different order*, which is a documentation page or a reference, not a shop.”
A page for another kind of business is the same sequence with different nodes in it
A registration declares the shape it draws, so the probe can generate an answer.
“Rejected for now because the schema lives *inside* the component rather than on the definition, so this is a change to definePrimitive and to every bound primitive — Loom primitives' lane, not this one — and because a generated value still has to be generated: an arbitrary instance of `{ title, detail?, meta?, href?”
Supersede 0019 now and build the canvas.
“The maintainer left room for it (*"there is room for some flexibility here"*) and also said the 90% was enough for now.”
The outline may render what it addresses, and a hand may yet move a node
A height budget per selector, failing the run when a block exceeds it.
“Rejected for now and not forever, for 0202's reason stated in its own last line: the first run of this instrument across six lanes' surfaces will find blocks below the fold on pages nobody considers broken, and a gate that goes red on all of them on the day the reading first exists is a gate somebody switches off.”
The harness reads a box, it prints the number, and the judgement stays in the report
Put the disposition on StoredRevision.
“Rejected here, and deliberately left open as a question rather than settled: 0016 makes the revision log the truth about the *page*, and whether a change reached outside the page is not a fact about the page.”
A structured reason travels with the judgment, and the prose is for readers only
A statistical test — a confidence interval or a two-proportion comparison.
“Rejected for now.”
A before-and-after reading compares two shares, and a pair the change dissolved is an answer
A row is here because the record's own paragraph says so, in the sentence printed under it. Nothing is judged for it: if a ruling changes its mind, this list changes with it, and if it never does, the row stays.
The cost nobody records
There is one more, and it is not in any of the rulings because it is not a decision anybody made: Loom is worth its price only if the same page changes more than once. A page that ships and then sits there gets the whole bill — the registry, the policy, the store, the log — and none of the benefit, which only arrives the fourth time somebody asks for something and nobody has to open an editor.
That is a judgement about your situation rather than about the framework, and it is the one thing on this page that no test can hold true.
Where the arguments are
Decision records is every ruling Loom has made, in the order they were made, with what each one replaced. The alternatives named on this page are all in there with their reasoning attached, and so are the several hundred that no cost above points at.