how it works
The event log
Leaf appends comments, widget actions, agent replies, reports, and version
records to events.jsonl. Earlier records remain when an action is
undone or retracted.
A widget's state starts with its authored markup. Leaf keys each action by
[widget, unit, verb]: the owning widget, the part of its state
being changed, and the operation. For each key, the latest action that has not
been undone or retracted sets that part of the state. Repeated choices in an
options group change the same selection unit, so a new pick replaces the
previous pick.
This page explains the mechanism. The rules themselves live in the events contract and the package contract's User state, which agents and package authors read.
Event validation and widget state
A widget submits an action. Leaf validates it against the package's registry declaration and appends it to the log. Leaf derives widget state from the authored initial values and logged actions. Packages declare their own verbs; Leaf processes those declarations without package-specific code.
flowchart TD
accTitle: Widget actions update widget state
accDescr: Leaf validates a widget's action and records it in events.jsonl. The authored initial values and logged actions determine the widget's state.
Widget["Widget"] -- "action" --> Door{"Validate and append"}
Door -- "record" --> Log[("events.jsonl")]
Log -- "derive state" --> Fold["State"]
Initial["Authored initial values"] --> Fold
Fold -- "renderState" --> Widget
| Layer | What it holds | Owner |
|---|---|---|
| Log | One record per line, append-only: messages and thread moves, actions and reports, undos, versions and sign-offs, and bookkeeping such as reads | Leaf |
| Event |
The command the sender wrote, plus the author,
id, and time Leaf adds, and a widget event's
meaning
|
Leaf |
| Verb |
A fold unit and a recorded effect or custom payload schema,
declared under x-state
|
Package, in registry.json |
| Widget | Submits actions and renders current state, including unset values | Package, in its module |
Try actions and undo
The sample below is a complete Leaf page with a log of its own, and its feed
lists that log newest first. Open Show activity, then type
After the canary is healthy into Another option, and press
Enter: press the notice counting new rows to reveal an
add and then a pick. Pick Later. Then press
z three times. The first press brings back the pick of the added
option, the second leaves nothing picked while the added option stays, and the
third removes the option. Press the activity notice to read the changed
history; it waits so arrivals never move the text being read. Reset starts the
sample over.
When should this ship?
Verb declarations
This is everything lf-options tells Leaf about its three verbs
and when its ask counts as answered.
"x-state": {
"choose": {
"unit": "widget",
"record": {"kind": "attribute", "attr": "chosen"}
},
"add": {
"detail": {"type": "object",
"properties": {"option": {"type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$"},
"text": {"type": "string", "minLength": 1}},
"required": ["option", "text"], "additionalProperties": false},
"unit": "option",
"creates": {"child": "lf-option", "words": "text"}
},
"answer": {"detail": {"type": "object", "additionalProperties": false}, "unit": "widget"}
},
"x-awaits": {
"value": "choose",
"answered": {
"choose": {"when": {"multiple": [false]}},
"answer": {"when": {"multiple": [true]}}
}
}
action.chosen on each
picked option. This effect also determines the payload:
detail.value
must be an array of owned option ids. The agent writes the pick into the
next version, and
page check refuses a version that contradicts it.detail.option, so each added option
has a coordinate of its own.lf-option to the group, with
detail.text as its text.answer is the Done press that closes a
multiple group.choose value.
An action's coordinate is its widget, its unit, and its verb.
unit: "widget" means the widget itself; any other value names the
detail field that holds the unit. A pick therefore uses
[ship, ship, choose], and adding an option uses
[ship, <new option id>, add]. Two actions compete only when
their coordinates are equal, so a pick never displaces an added option.
Every verb the kernel and the bundled packages declare
| Widget | Verb | Unit | Record in markup |
|---|---|---|---|
lf-suggestion | decide | widget | none |
lf-options | choose | widget | chosen attribute |
lf-options | add | option | creates an lf-option |
lf-options | answer | widget | none |
lf-board | move | unit | position in an lf-column |
lf-draft | edit | widget | body |
lf-playground | choose | widget | none |
lf-visual-review | review | case | none |
Widget state after each action
The table shows the options group after each action and undo in the sample.
| Gesture | Coordinate | The group shows |
|---|---|---|
| Enter in Another option |
add on the new option, then choose on
ship
|
Now, Later, and the new option, picked |
| Pick Later | choose on ship |
Later picked. It supersedes the first pick; the added option stays, because it has a different coordinate. |
| z | undoes the pick of Later | The new option picked again, restoring the previous selection |
| z | undoes the first pick | Nothing picked; the new option is still there |
| z | undoes the add |
Now and Later, nothing picked |
leaf page events printed the log below, one record per line; here
each record is broken over a few lines. The first line of the log, at
seq 1, is the note that published the page's first
version. The browser generated the added option's id and each
attempt.
{"kind": "action", "revision": 1, "widget": "ship", "action": "add",
"detail": {"option": "ship-option-1f82bf7ba6828718a418aa6076e29c63",
"text": "After the canary is healthy"},
"attempt": "1f82bf7ba6828718a418aa6076e29c63", "author": "user",
"id": "f541943a",
"meaning": {"scope": "page",
"unit": "ship-option-1f82bf7ba6828718a418aa6076e29c63",
"depends": ["ship", "ship-option-1f82bf7ba6828718a418aa6076e29c63"],
"state": {"origin": "lf-options",
"unit": "option",
"record": null,
"creates": {"child": "lf-option", "words": "text"},
"detail": {"type": "object",
"properties": {"option": {"type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$", "minLength": 1},
"text": {"type": "string", "minLength": 1}},
"required": ["option", "text"],
"additionalProperties": false}},
"creates": "lf-option"},
"attention": false, "ts": "2026-09-23T22:09:53-07:00", "seq": 2}
{"kind": "action", "revision": 1, "widget": "ship", "action": "choose",
"detail": {"value": ["ship-option-1f82bf7ba6828718a418aa6076e29c63"]},
"attempt": "d10bc95f51e47796494c2aa01b7c872f", "author": "user",
"id": "c47d1659",
"meaning": {"scope": "page",
"unit": "ship",
"depends": ["ship", "ship-option-1f82bf7ba6828718a418aa6076e29c63"],
"state": {"origin": "lf-options",
"unit": "widget",
"record": {"kind": "attribute", "attr": "chosen"},
"creates": null,
"detail": {"type": "object",
"properties": {"value": {"type": "array",
"items": {"type": "string", "minLength": 1},
"uniqueItems": true}},
"required": ["value"],
"additionalProperties": false}},
"answer": null},
"attention": true, "ts": "2026-09-23T22:09:53-07:00", "seq": 3}
{"kind": "action", "revision": 1, "widget": "ship", "action": "choose",
"detail": {"value": ["ship-later"]},
"attempt": "26e7a30143df35cda03ec8b7b164f2b6", "author": "user",
"id": "0450d79c",
"meaning": {"scope": "page",
"unit": "ship",
"depends": ["ship", "ship-later"],
"state": {"origin": "lf-options",
"unit": "widget",
"record": {"kind": "attribute", "attr": "chosen"},
"creates": null,
"detail": {"type": "object",
"properties": {"value": {"type": "array",
"items": {"type": "string", "minLength": 1},
"uniqueItems": true}},
"required": ["value"],
"additionalProperties": false}},
"answer": null},
"attention": true, "ts": "2026-09-23T22:09:54-07:00", "seq": 4}
{"kind": "undo", "attempt": "abec07d894a0444733756c659d4edafe", "undoes": "0450d79c", "author": "user",
"id": "6d9e1f06", "attention": true, "ts": "2026-09-23T22:09:54-07:00", "seq": 5}
{"kind": "undo", "attempt": "5a3b1fb07f32ba1933b472b288a03e36", "undoes": "c47d1659", "author": "user",
"id": "935db19d", "attention": true, "ts": "2026-09-23T22:09:55-07:00", "seq": 6}
{"kind": "undo", "attempt": "7d324c8ea06016e59db0ca09ea0303e4", "undoes": "f541943a", "author": "user",
"id": "b0f9f750", "attention": false, "ts": "2026-09-23T22:09:55-07:00", "seq": 7}
kind, revision,
widget, action, detail, and
attempt, its retry key. Leaf added the remaining fields.
attention records whether the action changes work the agent
owes.meaning and refuses a sender that supplies it. It
keeps only what a reader without the registry could not recover: whether the
action was made on the page or in a thread (scope), its fold
unit, the element ids it depends on, the child tag
it creates, and the operation's state definition.
That definition records the operation and its allowed detail values so later
revisions can tell whether the decision still applies.id; seq is the
record's line in the log.null means it closes no thread; a group with a
resolves attribute would name one here.Replacing and withdrawing actions
- A later action at the same coordinate supersedes it, as the pick of Later did. Undoing the later action restores the earlier one.
-
The user's
undowithdraws one gesture of their own: an action, a resolve or reopen, or an approval. Sent messages cannot be withdrawn. -
The author's
restated: a stamped version lists element ids it deliberately rewrote, and every earlier action that depends on one of them is retracted.
Once the agent records a current decision in the markup, such as the added
option as an ordinary lf-option with the same id and words, the
document carries it and the action no longer changes what the page shows.
Agent-reported state
A verb declared with "writer": "agent" accepts reports from the
agent. leaf page report appends a report event,
which updates the widget until a stamped version settles the report. The
package contract's
User state
gives the complete rules.
Following new events
leaf page events PAGE --follow prints every stored record, then
each new record as Leaf appends it. To resume after the last printed record,
pass its seq value to --after.
Typed external data under data/ is replaced in place rather than
logged, so the feed does not carry it. The events contract's
Following the log
gives the feed's whole contract.
Where each rule is written
-
Events and threads: every event kind, undo, admission and
meaning, and the change feed. -
User state: how a package declares
x-stateverbs,x-awaits.answered,creates, and agent-written verbs. -
Registry keys: the exact definition of
x-state,x-awaits, and the keys beside them. - Layer composition: which recorded actions a re-vendored layer must still be able to read.