how it works

The event log

Every move a user makes on a Leaf page, and every message, report, and version the agent adds, is one JSON record appended to the page's events.jsonl, and nothing is ever removed from it. A widget's state is the authored markup plus a fold of that log: at each [widget, unit, verb] coordinate, the latest action still standing wins.

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.

Leaf owns the log, and a package owns its verbs

A widget sends a verb. Leaf checks it at the one append door, stores it, and folds the log back into the state the widget draws. Nothing in Leaf's server or runtime names a package's verbs.

flowchart LR
  Widget["Widget"] -- "verb" --> Door{"Append door"}
  Door -- "record" --> Log[("events.jsonl")]
  Log -- "fold" --> Fold["State"]
  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, requests and receipts, and bookkeeping such as reads Leaf
Event The command the sender wrote, plus the author, id, and time the door adds, and a widget event's meaning Leaf
Verb A detail schema, a fold unit, and how the state reads in markup, declared under x-state Package, in registry.json
Widget Sends verbs, and draws the folded state, unset values included Package, in its module

Try it: each gesture lands as one event

The specimen below is a complete Leaf page with a log of its own, and its feed lists that log newest first. Click into the specimen, type After the canary is healthy into Another option, and press Enter: the feed gains an add and then a pick. Pick Later. Then press z three times. The first press brings your own option's pick back, the second leaves nothing picked while your option stays, and the third removes the option. Reset starts the specimen over.

A verb is a registry declaration

This is everything lf-options tells Leaf about its three verbs and when its question counts as answered.

"x-state": {
  "choose": {
    "detail": {"type": "object", "properties": {"options": {"type": "array", "items": {"type": "string"}}},
               "required": ["options"], "additionalProperties": false},
    "unit": "widget",
    "record": {"kind": "attribute", "attr": "chosen", "value": "options"}
  },
  "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": {
  "answered": {
    "choose": {"when": {"multiple": [false]}},
    "answer": {"when": {"multiple": [true]}}
  }
}
The verb's name, which the widget sends as the event's action. The JSON Schema every choose detail must satisfy at the door. One unit for the whole group, so every pick lands on the same coordinate and a later pick replaces an earlier one. How the standing pick reads in markup: chosen on each picked option. The agent writes it into the next version, and version check refuses a version that contradicts it. The unit is the value of detail.option, so each added option stands on a coordinate of its own. The action brings a real lf-option into the group, with detail.text as its words. answer is the Done press that closes a multiple group. The Ask is answered while this condition on the standing state holds: a standing pick for a single-choice group, a standing Done for a multiple one.

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 lands on [ship, ship, choose], and adding an option lands on [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 card position in an lf-column
lf-draft edit widget body
lf-diff review file none
lf-playground choose widget none
lf-swipe-deck swipe card position in an lf-swipe-pile
lf-targeting submit widget none
lf-visual-review review case none
lf-task status widget status attribute, written by the agent
lf-agent state widget state attribute, written by the agent

The fold keeps the latest standing action at each coordinate

These are the specimen's gestures above, made in Chrome against a copy of the specimen's page. The group shows the fold of whatever still stands after each one.

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 stands on another coordinate.
z undoes the pick of Later The new option picked again, since the pick it superseded stands
z undoes the first pick Nothing picked; the new option is still there
z undoes the add Now and Later, nothing picked

leaf 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 minted 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",
 "meaning": {"scope": "page",
   "unit": "ship-option-1f82bf7ba6828718a418aa6076e29c63",
   "depends": ["ship", "ship-option-1f82bf7ba6828718a418aa6076e29c63"],
   "creates": "lf-option"},
 "id": "f541943a", "ts": "2026-09-23T22:09:53-07:00", "seq": 2}
{"kind": "action", "revision": 1, "widget": "ship", "action": "choose",
 "detail": {"options": ["ship-option-1f82bf7ba6828718a418aa6076e29c63"]},
 "attempt": "d10bc95f51e47796494c2aa01b7c872f", "author": "user",
 "meaning": {"scope": "page",
   "unit": "ship",
   "depends": ["ship", "ship-option-1f82bf7ba6828718a418aa6076e29c63"],
   "answer": null},
 "id": "c47d1659", "ts": "2026-09-23T22:09:53-07:00", "seq": 3}
{"kind": "action", "revision": 1, "widget": "ship", "action": "choose",
 "detail": {"options": ["ship-later"]},
 "attempt": "26e7a30143df35cda03ec8b7b164f2b6", "author": "user",
 "meaning": {"scope": "page", "unit": "ship", "depends": ["ship", "ship-later"], "answer": null},
 "id": "0450d79c", "ts": "2026-09-23T22:09:54-07:00", "seq": 4}
{"kind": "undo", "undoes": "0450d79c", "attempt": "abec07d894a0444733756c659d4edafe", "author": "user",
 "id": "6d9e1f06", "ts": "2026-09-23T22:09:54-07:00", "seq": 5}
{"kind": "undo", "undoes": "c47d1659", "attempt": "5a3b1fb07f32ba1933b472b288a03e36", "author": "user",
 "id": "935db19d", "ts": "2026-09-23T22:09:55-07:00", "seq": 6}
{"kind": "undo", "undoes": "f541943a", "attempt": "7d324c8ea06016e59db0ca09ea0303e4", "author": "user",
 "id": "b0f9f750", "ts": "2026-09-23T22:09:55-07:00", "seq": 7}
The browser sent kind, revision, widget, action, detail, and attempt, its retry key. The door stamped the rest. The door 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, and the child tag it creates. The door mints the eight-hex id; seq is the record's line in the log. This pick makes the Ask's answered condition hold, so it is the answer. null means it closes no thread; a group with a resolves attribute would name one here. An undo names one gesture and nothing else. The folds drop the gesture it names, and both records stay in the log.

Three things take an action back, and none of them deletes it

Once the agent writes a standing decision into 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.

An agent's state moves through the same fold

A verb that declares "writer": "agent" is the agent's: leaf report appends a report at a coordinate like an action's, and it stands until a stamped version answers it. The package contract's User state gives the whole rule.

A program can follow the log as it grows

leaf events PAGE --follow prints every stored record and then each one the door admits as it lands, and a reader that restarts with --after the last seq it printed picks up where it stopped.

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