Authoring OKF nodes with okfctl
Overview
okfctl manages OKF bundles — directories of Markdown "nodes" that form a
linked knowledge graph. Each concept node is a Markdown file with YAML front
matter (type, title, optional aliases) and a prose body; links between
nodes are ordinary relative Markdown links, and the path is the node's
identity. This skill is the author's loop: create a bundle, add and edit nodes,
move or remove them safely, and maintain the two reserved files (index.md,
log.md).
Everything here needs only the okfctl binary — no model, no network, no index.
When to Use
- You are starting a new bundle or adding/editing/renaming nodes in one.
- You want new nodes to conform to a team convention (a type template).
- You need to move or delete a node without breaking inbound links.
- You are keeping
index.md(the generated table of contents) current.
Don't use for: curation-health findings (orphans, coverage gaps) — see
okf-curation-health; semantic similarity checks — see okf-semantic-search.
Build the binary
git clone <the okfctl repo>
cd okfctl
CGO_ENABLED=0 go build -o okfctl .
# put it on PATH, e.g. `sudo mv okfctl /usr/local/bin/`, or invoke as ./okfctl1. Create a bundle
$ okfctl bundle init mykb
Initialized OKF bundle in mykbA fresh bundle contains exactly three files:
mykb/.okf # bundle marker: `okf_version: 0.1`
mykb/index.md # RESERVED — generated table of contents (type: Index)
mykb/log.md # RESERVED — change historybundle info summarizes any bundle:
$ okfctl bundle info mykb
nodes: 0
reserved: 2
okf_version: 0.12. Author concept nodes
$ okfctl node new concepts/tannin.md --type Reference --title "Tannin" --bundle mykb
Created mykb/concepts/tannin.mdThe file it writes:
---
type: Reference
title: Tannin
---
# Tannin--type accepts any string — OKF is anti-taxonomy at the spec floor, so
Reference, Concept, or any other value all pass validate. Inspect and list:
$ okfctl node show concepts/tannin.md --bundle mykb
path: concepts/tannin.md
type: Reference
# Tannin
$ okfctl node list --bundle mykb
concepts/mouthfeel.md Concept
concepts/tannin.md ReferenceLink nodes with ordinary relative Markdown links in the body — [Tannin](tannin.md)
from a sibling file. Links are what make the graph traversable (and what keeps a
node off the orphan list; see okf-curation-health).
Edit a node in $EDITOR (falls back to $OKFCTL_EDITOR / $VISUAL); okfctl
re-validates the bundle when the editor returns:
$ okfctl node edit concepts/tannin.md --bundle mykb3. Type templates (optional team conventions)
A type template is an ordinary OKF node with type: Type Template. Nothing
about a template lives in tool config — it is data in the bundle. It declares a
target_type and the fields/sections nodes of that type should carry:
---
type: Type Template
target_type: Playbook
required_fields: [title, description, owner]
recommended_fields: [tags]
body_sections: [Trigger, Steps, Rollback, Verification]
---
# Playbook TemplateInspect the templates a bundle declares:
$ okfctl template list mykb
Playbook 3 required field(s), 4 body section(s)
$ okfctl template show Playbook mykb
target_type: Playbook
source: templates/playbook.md
required_fields: title, description, owner
recommended_fields: tags
body_sections: Trigger, Steps, Rollback, VerificationWhen a template governs a --type, node new scaffolds from it: required
fields are stubbed with TODO, recommended fields are stubbed empty, and each
declared body section is laid down as an empty ## heading, so the node starts
free of template drift:
$ okfctl node new playbooks/restart.md --type Playbook --title "Restart Service" --bundle mykb
Created mykb/playbooks/restart.md (from Playbook template)---
type: Playbook
title: Restart Service
description: TODO
owner: TODO
tags:
---
# Restart Service
## Trigger
## Steps
## Rollback
## Verification(Checking a node against its template is a curation concern — validate --templates; see okf-curation-health.)
4. Move and remove nodes safely
The node's path is its identity, so a move is a graph operation: node mv
rewrites every inbound link and preserves each author's relative link form.
Preview with --dry-run first:
$ okfctl node mv concepts/oak.md concepts/oak-aging.md --bundle mykb --dry-run
move concepts/oak.md -> concepts/oak-aging.md
$ okfctl node mv concepts/oak.md concepts/oak-aging.md --bundle mykb
Moved concepts/oak.md -> concepts/oak-aging.md (0 inbound link(s) rewritten)node rm reports which nodes would become orphaned as a result. Always
--dry-run before a real removal:
$ okfctl node rm concepts/mouthfeel.md --bundle mykb --dry-run
remove concepts/mouthfeel.md
orphaned: concepts/tannin.mdMigrating a directory-as-concept corpus with node promote
Corpora imported from tools where a directory is a concept — Obsidian folder
notes, Hugo _index.md, Jekyll collections, most wikis — put the concept's
frontmatter and body in each directory's index.md. OKF models index.md
differently: it is a generated navigation surface (§8), and only the bundle-root
index may carry frontmatter (the §12 okf_version marker). So every non-root
index.md with frontmatter fails validate, and on first contact that can be
hundreds of identical findings.
node promote is the one-command remediation. For every non-root index.md
that carries frontmatter it moves the file to a sibling concept
(foo/index.md → foo/foo.md), preserves the body verbatim and keeps created
immutable, rewrites inbound links (both the foo/ and foo/index.md
spellings), regenerates the real index.md with no frontmatter, and appends to
log.md. The bundle-root index is left alone. Always --dry-run first — it
lists every move and link rewrite and writes nothing:
$ okfctl node promote mykb --dry-run
would promote gke-pm-map/index.md -> gke-pm-map/gke-pm-map.md
rewrite overview.md: gke-pm-map/ -> gke-pm-map/gke-pm-map.md
2 index(es) would be promoted (dry run; nothing written)
$ okfctl node promote mykb --name overview # one basename convention for allVerify the migration broke no links: okfctl lint mykb --strict must report
zero broken-link findings afterward (see okf-curation-health).
5. The reserved files: index.md and log.md
index.md and log.md are reserved — they are structurally different from
concept nodes and the tool owns their shape. index.md is a generated table of
contents; regenerate it whenever nodes change, and verify it is current in CI:
$ okfctl index build mykb
Wrote mykb/index.md
$ okfctl index check mykb # exit 0 when current
OK: index.md is current
$ okfctl node new concepts/oak.md --type Concept --title Oak --bundle mykb
$ okfctl index check mykb # exit 1 when stale
index.md is out of date; run `okfctl index build` to regenerateA rebuilt index.md links every node, which is also how a bundle's nodes stop
reading as orphans — index.md confers reachability (see okf-curation-health).
Record changes in log.md:
$ okfctl log append mykb --message "added oak node"
Appended log entry
$ okfctl log show mykb
# Change Log
- 2026-07-26 — added oak nodeCommon Pitfalls
Treating index.md / log.md as concept nodes. They are reserved:
index.mdis generated (don't hand-edit —index buildoverwrites it) andlog.mdlegitimately has no inbound links. Curation tools know this and never flag the reserved files as orphans; only concept nodes participate in graph checks.Hand-editing index.md. Any manual edit is lost on the next
index buildand reads as "out of date" toindex checkuntil rebuilt. Author content in concept nodes; letindex buildownindex.md.Expecting
node newto require a known type. It doesn't — any--typestring is accepted at the spec floor. Type conventions are opt-in via type templates, not enforced by the taxonomy.Moving a node with
mv/git mvinstead ofokfctl node mv. A raw move leaves every inbound link pointing at the old path.okfctl node mvrewrites them; a raw move silently creates broken links (and orphans).Deleting a node without
--dry-run.node rm --dry-runtells you which nodes the removal orphans before you commit to it — cheap insurance against silently cutting a node loose from the graph.Forgetting to rebuild index.md after adding nodes. New nodes are absent from the table of contents and read as orphans until
index buildlinks them. Runindex build(andindex checkin CI) as part of the authoring loop.
Verification Checklist
-
okfctl bundle info <dir>shows the expected node / reserved counts - New nodes appear in
okfctl node list --bundle <dir> - Nodes intended to follow a convention were scaffolded from a type template (output says "from <Type> template")
- Every
node mv/node rmwas previewed with--dry-runfirst -
okfctl index check <dir>exits 0 (index.md current) after authoring - Changes recorded with
okfctl log append