Automation and AI tools¶
leo-cub is designed to give scripts and coding agents a small, explicit
interface to an outline. A useful automation loop is:
- Inspect the relevant subtree as JSON.
- Construct a small operation batch.
- Apply it with
--dry-run. - Apply the same batch for real.
- Validate the resulting outline.
cub inspect project.leo --format json > before.json
cub apply project.leo operations.json --dry-run
cub apply project.leo operations.json
cub validate project.leo
An operation batch is a JSON object with an operations array. For example:
{
"operations": [
{
"op": "set-body",
"node": "ekr.20260811210000.1",
"expected": "old body",
"body": "new body"
}
]
}
The GNX in a real operation must come from the outline being edited. The
expected field provides conflict detection; omit it only when overwriting
the current value is intentional.
Adding a whole tree at once¶
Building structure — rather than editing existing nodes — with one insert
per node means threading parent GNXs through the whole batch by hand. The
insert-tree operation instead takes a nested dict, so a script can just
build the structure it wants and hand it over in one shot:
{
"gnx-prefix": "acme",
"operations": [
{
"op": "insert-tree",
"parent": "ekr.1",
"tree": {
"Milestones": {
"_body": "",
"Kickoff": { "_body": "Draft agenda." },
"Beta": { "_body": "Target date TBD." }
}
}
}
]
}
_gnx is optional per node; omitted ones get a fresh id built from the
batch's gnx-prefix (default "cub") — pass your own prefix to keep
scripted nodes easy to spot later. _body defaults to "". Since the tree
is a JSON object, siblings come out sorted by headline rather than in
writing order.
Targeting a parent by headline that might not exist yet¶
A recurring import script — pulling in GitHub PRs or issues, say — usually
knows a stable destination like "Imports/PRs" but not its GNX, and the
destination may not exist on the first run. insert-tree and merge-tree
accept "parent-headline" in place of "parent" for exactly this: it
resolves the path the same way cub add does, reusing any prefix that
already exists, and creates whatever segments are missing instead of
failing:
{
"operations": [
{
"op": "insert-tree",
"parent-headline": "Imports/PRs",
"tree": {
"PR #142: Fix flaky retry": { "_body": "https://github.com/.../142" }
}
}
]
}
Running that batch again with a different PR under the same
"parent-headline" reuses the existing Imports/PRs nodes rather than
creating duplicates. Give at most one of "parent"/"parent-headline";
omitting both targets the outline root.
Regenerating a section by its headline¶
A script that regenerates content — a changelog, a generated report section —
usually knows the section's headline but not its GNX, and doesn't care about
keeping the old GNX around. replace-tree removes the node at a headline
path (or GNX) along with its whole subtree, then inserts a fresh
insert-tree-shaped tree in the same spot:
{
"operations": [
{
"op": "replace-tree",
"headline": "Docs/Changelog",
"tree": {
"Changelog": {
"_body": "Regenerated from the latest release notes.",
"0.4.0": { "_body": "..." }
}
}
}
]
}
The headline path is resolved the same way as cub add's paths, and fails
the batch if it's ambiguous or missing. The replaced node's GNX is not
reused; the new tree gets fresh ids the same way insert-tree does.
Merging into a section without discarding it¶
replace-tree is destructive: everything under the target headline is
gone before the new tree goes in. When a script instead wants to update or
extend an existing section — bump a body, add a new child — without
touching siblings it doesn't know about, merge-tree matches tree's
entries against parent's existing children by headline:
{
"operations": [
{
"op": "merge-tree",
"parent": "ekr.1",
"tree": {
"Milestones": {
"Kickoff": { "_body": "Draft agenda — updated." },
"Launch": { "_body": "New milestone." }
}
}
}
]
}
A matching headline gets its body updated only if _body is given (leaving
it out preserves the existing body) and its children merged the same way,
recursively; a headline with no match is inserted fresh. merge-tree never
deletes a node — an entry not mentioned in tree is left exactly as-is.
Cloning a node¶
clone inserts a new occurrence of a node that already exists elsewhere in
the outline, rather than creating a new node. The clone starts out with a
copy of the source occurrence's children, so it never diverges from the node
it was cloned from:
{
"operations": [
{
"op": "clone",
"parent-headline": "Team B",
"index": 0,
"node": "ekr.20260811210000.7"
}
]
}
"node" is the GNX of the node to clone — not a headline path, since
cloning only makes sense for a node that already has an identity. "parent"
(a GNX) and "parent-headline" work the same way as insert-tree: give at
most one, omit both to clone to the outline root, and "parent-headline"
creates any missing segments of the path the same way cub add does.
A Rhai script can reach for
doc.clone_node(gnx, parent_gnx) instead of building this JSON by hand;
unlike this JSON form it takes the parent as a gnx rather than a headline
path, so resolve "parent-headline"-style paths with doc.gnx/doc.add
first if that's what the script has.
Because a clone is another occurrence of the same node, editing the
headline, body, or children through either occurrence — the original or the
new one — changes both, the same way editing any clone does in the Leo
outline format. cub render marks every occurrence but the first with
↪ clone so it's clear which one is which in output.
Search before loading¶
For large outlines, search headlines and body text directly:
Search output includes excerpts rather than dumping every matching body. This keeps agent context and command output manageable.
Embed an outline in Zensical¶
The outline renderer emits ordinary Markdown, so Zensical can render it during the documentation build with the Markdown Exec extension:
Then include the selected outline in a page:
Only headlines are emitted. Repeated vnode occurrences are marked with
↪ clone, while their descendants are not repeated.
Practical guardrails¶
- Keep the original
.leofile or use version control before scripted edits. - Prefer one focused batch over a long sequence of independent mutations.
- Use
--dry-runfor imports, syncs, and operation batches when available. - Run
cub validateafter a write.