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.

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]}}
  }
}
The verb's name, which the widget sends as the event's action. One unit for the whole group, so every pick uses the same coordinate and a later pick replaces an earlier one. How the current selection appears in markup: 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. The unit is the value of detail.option, so each added option has a coordinate of its own. The action adds an lf-option to the group, with detail.text as its text. answer is the Done press that closes a multiple group. The ask's typed answer comes from the current choose value. A single-choice group is answered when an option is selected. A multiple-choice group is answered when the user presses Done. Undo can make the ask unanswered again.

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-suggestiondecidewidgetnone
lf-optionschoosewidgetchosen attribute
lf-optionsaddoptioncreates an lf-option
lf-optionsanswerwidgetnone
lf-boardmoveunitposition in an lf-column
lf-drafteditwidgetbody
lf-playgroundchoosewidgetnone
lf-visual-reviewreviewcasenone

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}
The browser sent 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. Leaf derives 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. Leaf generates the eight-hex id; seq is the record's line in the log. This pick makes the ask's answered condition hold, so it completes the ask. null means it closes no thread; a group with a resolves attribute would name one here. An undo names the gesture to withdraw. Leaf excludes that gesture when deriving state, and both records stay in the log.

Replacing and withdrawing actions

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