LifeLab · block model

Block properties — one surface

Two ways to attach data to a block have drifted apart. Metadata (right-click → show) is a free-form JSON bag — powerful, but hidden, ugly, and it evaporates when you collapse it. Drawer properties (defined on a tag's page) render as a tidy :PROPERTIES: drawer and stay consistent — but you author them somewhere else, and a block ends up carrying both drawers with the same values duplicated.

The fix is one store and one surface. Every block already holds a key/value bag; the question is only how you see and edit it. These frames collapse the two systems into a single properties drawer, add a fast inline :key=value: path, and demote the plumbing (id, timestamps, ui flags) out of sight.

Add · inline :k=v:

Type :amount=400: on any headline — on ↵ it commits into properties and leaves the title clean.

See · one drawer

Schema fields and adhoc fields share one :PROPERTIES: drawer. Collapse summarizes — never deletes.

Define · tag page

The tag's schema is the one show-list: declare a field there and every tagged block gets it.

Inspect · everything

id / timestamps / plugin blobs never touch the drawer — the inspector shows the whole bag on demand.

nowTHE SNAG

Two drawers, one block, duplicated data

The same amount:400 lives in both the pretty :PROPERTIES: drawer and the raw :METADATA: bag — alongside plumbing you never wanted to see (block_id, _tag_property_types, collapsed). Two mental models fighting over one block.

Home
*vuori and abercrombie shopping :spending:
:PROPERTIES:
:amount:400
:regret:3
:END:
:METADATA:
:BLOCK_ID:4d1cbd46-c297-498c…
:CREATED_AT:4/26/2026 07:08 PM
_tag_property_types{"amount":"number","regret":"number"}
amount400
collapsedtrue
regret3
:END:
Three problems. amount and regret appear twice. Plumbing (block_id, _tag_property_types, collapsed) is mixed in with real data. And the whole :METADATA: block is only reachable by right-click → Show Metadata — collapse it and it's gone.
AADD · inline :key=value:

Type it in the title — it lands in properties

The fastest path for one-off tracking. Type :key=value: anywhere in the title; on it commits into the block's property store and dissolves from the title text. One home for data (properties), one quick way in (typing). The = separates it from a plain :tag: only while you type.

Home
after ↵ · token dissolves — value shows as a chip, title stays clean
*morning run :health: miles4.2
typing · : fires completion
*vuori and abercrombie shopping :spending: :amount=4
properties= makes it a property
:amount=number · seen 24×
:amazon=number · seen 11×
:amount=400:set value
accept=value:plain tag instead
Input grammar, not storage. The token is an entry affordance — after commit no second syntax lives in your titles and there's no tag/property ambiguity at rest. Display is governed by the tiers (frame F); search and templates read the property store, not title text.
BSEE · one :PROPERTIES: drawer

Schema + adhoc together, plumbing gone

Every property — whether it came from a tag schema or was typed inline — renders in a single drawer. Schema fields carry a faint origin tag and a type; adhoc fields carry a plum dot. System plumbing never appears here at all — it lives only in the inspector (frame I).

Home
*vuori and abercrombie shopping :spending:
:PROPERTIES:
amount:spending:400 number
regret:spending:3 number
storeadhocvuori text
+ add property · type :k=v: in the title for the same result
:END:
collapsed — nothing lost
*vuori and abercrombie shopping :spending:
amount400· regret3· storevuori
Collapse is a summary, not a delete. Today, collapsing metadata hides it so completely it feels gone. Here the collapsed state shows an inline amount 400 · regret 3 strip — glanceable in a list, one click to expand. The data is always attached; only the chrome folds.
open questions — resolved
HSHOW-LIST · the schema decides

What renders in :PROPERTIES: = the tag's schema

_tag_property_types had the right idea — a declared list of what to show — its only sin was living as a copy on every block. Keep the gate, move it to the tag page. logbook's blob shows nowhere because no schema declares it — no registry, no naming convention, no smarts. And when the plugin wants a visible scalar, it publishes one into the :logbook: schema.

*Mandarin study :logbook:
:LOGBOOK:reads & writes key logbook — undeclared, so the drawer ignores it
CLOCK IN
CLOCK: [2026-07-09 Thu 00:38] -- [2026-07-09 Thu 00:42] => 00:04
CLOCK: [2026-07-08 Wed 00:51] -- [2026-07-08 Wed 00:52] => 00:02
CLOCK: [2026-07-05 Sun 00:41] -- [2026-07-05 Sun 00:46] => 00:04
⋮ 26 more entries
Total: 05:30 (5h 30m)
:END:
:PROPERTIES:
level:logbook:HSK 3 text
total:logbook:5:30 duration · written by plugin
:END:
Rule per key: declared in a tag schema → drawer. Typed inline → committed to properties, shown by tier. Anything else → inspector only. The blob never leaks; the plugin surfaces total deliberately, as a typed schema field every :logbook: block shares.
ITRANSPARENCY · the inspector

"All the data in a block" — one keystroke away

What you liked about :METADATA: was honesty, not the drawer. Keep the honesty, drop the drawer: Mod+I on any block opens the inspector — the entire bag, grouped by owner, raw values, editable. It replaces right-click → Show Metadata and never costs vertical space in the document.

inspect · Mandarin studyff206fe5-7741…
authored — shown in :PROPERTIES:
level"HSK 3"
claimed — rendered by :LOGBOOK:
logbook{"entries": [ 29 items · 4.1 kb — click to unfold ]}
system — hidden
block_id"ff206fe5-7741-4844-8612…"
created_at2/24/2026 01:45 AM
updated_at7/9/2026 12:42 AM
auto_runtrue
collapsedfalse
e edit rawy copy json7 keys · nothing is hidden from you
Separate AND unified. One store underneath (unify). Each key displayed by exactly one owner — plugin, properties, or nowhere (separate). The inspector is the single truthful view over all of it, replacing the metadata drawer entirely.
DTYPE · no code needed

:k=v: gets its type from the value

You worried inline props can't declare number vs text without code. They don't have to — the literal decides. A bare number is a number, true/false is a boolean, a recognizable date is a date, anything else is text. Quote to force text: :zip="00501":. The schema is where you pin a type when inference guesses wrong — declared once on the tag page.

:amount=400: number digits only → number, aggregatable
:done=true: boolean true/false/yes/no → boolean
:due=fri: date parses as a day → date, sortable
:store=vuori: text anything else → text
:zip="00501": text quotes force text, keep the 0
Inference for adhoc, declaration for schema. 95% of props never touch a type picker. The 5% you care about get pinned once on the tag page and every block inherits it.
ESHOW/HIDE · one copy of the gate

Move the gate to the tag page, not out of the system

Per-block _tag_property_types did two jobs — declare types and decide what renders — and was copied onto every block. Keep both jobs, in one place: the tag's schema (frame H). Anything the bag holds that no schema declares and no human typed inline stays unlisted. System plumbing never competes for the drawer — it lives only in the inspector.

authored — always shown
amount400
regret3
storevuori
Default-hidden is safe for programs. A plugin can write anything to the bag without polluting the view — nothing shows unless a schema declares it or a human typed it. The inspector (frame I) still shows every key, so nothing is ever invisible.
FDENSITY · drawer is the exception

Three tiers, cheapest by default

You're right that a drawer on every block eats vertical space. So the drawer isn't the default — it's the third tier you open on demand. When a block has few props, chips render on the title line itself at zero extra height — display from the store, not text; the collapsed strip is one line; the full drawer only appears when you're editing.

Title line
+0 lines · chips, not text
* morning run :health: miles4.2
Summary
+1 line · in lists
amount400· regret3· storevuori
Drawer
on demand · edit
:PROPERTIES:
amount:spending:400 number
storeadhocvuori text
The list stays scannable. A page of blocks shows inline tokens and one-line strips; you drop into the drawer only for the block you're actually editing.
GPERF · the := switch

Autocomplete reads an index, never loads pages

Your lag worry is real, but it's not the grammar — it's that typing : hydrates pages for tag completion. Fix the eager load. Completion queries a tiny in-memory index of tag names and property keys. Typing = just filters the same list to property mode. Nothing heavy ever started, so nothing is thrown away.

: query merged index — tags + property keys~1ms · in memory
sp filter the same list in placefree
= narrow to property keys — no re-fetchfree
: old: hydrate pages for tag previewdozens of reads
Page hydration is a hover/open concern, not a keystroke one. Load a page's contents only when the user actually opens or previews it — never to populate a dropdown. The index is rebuilt in the background when tags or keys change.
JDENSITY · which tier, when

Focus decides — the drawer follows the cursor

No per-block display setting to manage. Title-line chips are display-only and always on when props fit. Blocks at rest show the one-line summary. Moving the cursor into a block expands its drawer in place; leaving folds it back to the strip. Tab pins a drawer open if you want it to stay — that pin is the only persisted state (a system flag, hidden like collapsed today).

at rest · summary strip
*vuori and abercrombie shopping :spending:
amount400· regret3
cursor inside · drawer expands in place
*gym membership :spending:
:PROPERTIES:
amount:spending:45 number
recurringadhocmonthly text
:END:
query / list views · capped at summary
*morning run :health: miles4.2
Same rule as org-mode folding: the document breathes around your point of attention. Lists and query results never pay for drawers; the block you're editing always gives you the full surface without asking.