A dashboard in our product has two authors. An agent builds it, and a routine refreshes it on a schedule, rewriting the numbers, the series, the rows. The person who owns it arranges it by hand: drags a panel wider, renames another, picks a colour, binds a card to a dataset. Both authors write to the same object, and they have opposite rules. What the machine wrote is supposed to be replaced wholesale on every refresh, because stale data is the failure. What the person decided is supposed to survive every refresh, because losing it is the failure. In our types, nothing distinguished the two. A panel's title and a panel's data sat as sibling fields in one bag, one belonging to a person and one to a pipeline, identical to every function that touched them.
This entry is about how we drew that line so code could see it, and what it turned out to be made of. The shape is not particular to dashboards. Any long-lived object that a machine refreshes and a human also edits carries this boundary, whether or not anyone has drawn it, and an agent that writes to something a person also owns is a co-author with no agreed division of the page.
Two authors, one object
What the design gives, in the order we understood we needed it.
Presentation lives beside the data, not inside it. A panel's colour, its density, its display options are stored on the dashboard document keyed by panel id, in a separate object from the panels themselves. A refresh that replaces the panel array wholesale cannot touch them, because it never sees them. This was the first author-owned facet to be separated out, and it was separated before we had a word for what we were doing; the argument at the time was only that a refresh should not have to know about colours.
The stored panels are the skeleton, and a refresh only fills it. A refresh re-renders a dashboard's panels from scratch: the agent composing the fresh output decides the panel order, each panel's width, and the panel set, every run. Left alone, that meant a refresh that was only meant to update the numbers could reflow the whole card, with a different order, a widened panel, one added, one gone. So the merge treats the previously stored panels as authoritative. The output is exactly the stored panels, in stored order, at stored widths, with only the data slotted in from the matching fresh panel. A panel the refresh tried to add is dropped. A panel the refresh omitted keeps its last good content.
The asymmetry is a property of routing, not a flag. The product rule is that an unattended refresh may change what a dashboard says and never how it is arranged; only an interactive edit, a person asking the agent to move or resize or add something, may restructure it. We did not implement that with a mode. Every scheduled refresh writes back through one path, and that path runs the skeleton merge. The interactive paths, the agent's own dashboard tools and a re-render requested in conversation, bypass it entirely. The agent can rearrange a dashboard when asked and cannot when merely refreshing, and that is true because of which door each kind of write comes through, with no state that could get out of sync.
On the read side, data comes from the view and presentation from the document. Dashboards are also assembled at read time: a drilled-down view of one record, a per-parameter instance, a mixed board whose panels each resolve from their own source. Each of those resolvers builds a dashboard from panel data, and each is a place to drop the author's presentation. So every resolver's output now passes through one function that re-attaches the document's presentation onto whatever the data resolution produced, the document's saved style winning over anything a branch made up.
What the boundary is made of
We did not design the four rules above in one sitting. Each was written on the day a refresh erased something a person had decided, and the sequence is the evidence for this entry's argument. At the end of June a scheduled refresh reflowed a dashboard's layout; the layout became the skeleton. Then a refresh wrote an instance's title over its template's; instance runs stopped writing to the template at all. Three days after that, a colour a person had just picked repainted and then snapped back before their eyes, because a drilled view's resolver rebuilt the dashboard from panel data alone; that was the read-side merge. A week later a refresh detached a panel from its dataset, because a routine's captured render reproduced the card as plain inline data and the merge took the fresh panel's binding, which was none; the binding joined the skeleton. And this week a refresh renamed two panels on a dashboard installed from one of our Kits, from the titles the Kit's author wrote to "Records" and "Trend".
Read side by side, the fixes have an uncomfortable shape. Each one protects one more field. Layout, then title, then presentation, then binding, then title again one level down. Each fix is correct, tested and local, and the boundary those fields share still existed nowhere except in the heads of the people who had been bitten by it. The type system cannot warn the next code path, because both kinds of field look identical to it. The regression tests protect exactly the fields already lost, because tests get written where blood was drawn. And new code paths that rebuild a dashboard from one side's data keep being written, because rebuilding from data is the natural shape of a resolver, and every author-owned field it does not know about defaults to gone. The merge is last-writer-wins by default, and the machine always writes last.
So the pin list is not a list of fields to protect. It is the boundary between data and authorship, and every time we treated it as the former, another author-owned field taught us the latter. The question to ask before adding any field to a panel is not "should this be pinned" but two questions together: can an author set this, and does a refresh regenerate it? A field that answers yes to both lives on the line and belongs behind the choke point from the day it exists.
And this week's incident showed that the line is not field-shaped at all. The panels that lost their titles were bound to a dataset, and a dataset panel is refreshed by a pure transform that turns rows into panels and has never seen the author's dashboard; its titles are pattern defaults, "Records" for a count, "Trend" for a line over time. Pinning the authored title for those panels is plainly right. But pinning the title for every panel would be wrong, because a panel rendered by a routine or an agent is produced by something that is an author: its title is legitimately part of what it made, and "Spend — July" must stay free to become "Spend — August". The same field, the title, is authorship on one panel and data on its neighbour, and the only thing that decides which is who produced it. So the rule that shipped pins a panel's title when, and only when, the panel's source is a dataset. The boundary is owner-shaped. The line we could not see was never between fields. It was between authors, and one of the authors is a machine.
When the machine reads its own mistake
This week's incident earns a section of its own for what happened after the rename. The generic title was not only displayed; it was written back into the stored document. The routine that refreshes that dashboard reads the document at the start of every run, to know what it is filling, and then renders its output. So on the next run it read "Records" out of the document, took it for the panel's name, and faithfully re-emitted it. A one-time slip in the merge had become a standing fact, ratified by the agent on every subsequent run. Reverting the document by hand would have lasted exactly one cycle.
That is a different class of failure from the four before it, and it is specific to systems where an agent reads back what it wrote. In a pipeline with no loop, a bad write is a bad write; you correct the data and move on. Where the writer is also a reader of its own output, the artefact is part of the prompt, and anything the merge lets through becomes the agent's instruction next time. The repair has to be in the merge, not in the data, because the data will be regenerated from the merge's last mistake. For anyone building agents that maintain documents, dashboards or tables they also consult, the loop through storage is a feedback path, and every field it carries is a field the agent can be taught the wrong value of by its own previous run.
What transfers
Inventory your long-lived objects for fields with different owners. Anything a machine refreshes half of and a person authored the other half of is carrying this boundary, drawn or not. The tells are cheap to check. Does any operation rebuild the object from one side's inputs? Do your fixes for "X got lost on refresh" take the form of pinning individual fields? Is there a list somewhere, in code or in folklore, of fields to preserve? A pin list is a boundary described one casualty at a time.
Express the boundary instead. Separate the owners in the types if you can. If you cannot, put the merge in one named function that takes the machine's fresh output and the author's stored document as separate arguments and produces the composite, so that dropping authorship stops being the default behaviour of forgetting and becomes something you would have to write. Route every rebuild through it. Then classify by owner rather than by field, because the same field can be authorship in one place and data in the next, and the only stable question is who produced it. And if the machine reads its own output, check which fields the loop can silently ratify; the fix for those is never in the data.
The difference, in the end, is whether the next engineer's new code path is safe by construction or safe by their having heard the story.
Postscript, September 2026. The boundary held, and a week later it was in the way. A Kit's dashboard is installed before its routine has gathered any data, so its first refresh runs over zero rows and stores a panel set with no breakdown in it; the transform that builds panels from a ledger only emits a category breakdown when there are rows to group. Every later refresh id-matched against that empty-day skeleton and dropped the breakdown as a panel the refresh had "tried to add". The layout lock was doing exactly what it was built to do, and for this dashboard it was wrong, because the panel set of a dashboard derived from a dataset is not anyone's decision. It is a fact about the data. So the merge now grows a dashboard's panel set from fresh output when, and only when, the dashboard's source is a dataset, keeping every stored panel's order, width and binding, and keeping any panel a person added that the transform did not produce. The rule that came out of it belongs beside the one above: before protecting a field, ask whether the thing you are protecting is the user's decision or a function of their data. Owner-shaped cuts both ways. A guard written for an author, applied to a transform, freezes the product to the day it was installed.