Runbook Parser
Turns operational runbook documents (wiki pages, markdown like crunchy-notes.md)
into a version-controlled, composable library of building blocks for n8n workflows, and
keeps the extracted code aligned with its source, surfacing drift so fixes can be ported back.
Two kinds of snippet:
- Raw (
working_dir/<group>/) — the parser losslessly decomposes each source into ordered, typed snippets named<source-name>_<section>.ext(prose to.md, fenced blocks to.sh/.yaml/.sql/…). Mostly useless alone, but recombining them reproduces the doc, which powers drift detection. - Cleaned (
snippets/<group>/) — you (the agent) rewrite chosen raw snippets intosnippet-*building blocks that are individually runnable, parameterized, read-only and idempotent.
config.yaml is the single manifest, shaped like a CRD: spec: is human-authored,
status: is machine-owned and written back by the parser. The tool is a small Python
package at scripts/parser.py (requires PyYAML and markdown-it-py); the markdown templating
helper is tools/n8n/render.sh.
Use When
- Authoring or extending a
RunbookParserconfig.yaml(new sources, merging same-name groups). Seereferences/CONFIG.md. - Pulling a wiki or markdown runbook into committed snippets under
snippets/. - Turning raw snippets into safe, parameterized cleaned
snippet-*blocks. - Checking drift between docs and extracted code (local or CI).
- Backporting a cleaned fix: when the seal breaks,
diffshows the change and the agent proposes the doc edit. - Adding a hand-authored cleaned snippet (no raw to start from) and sealing it in one step —
include. - Running the parser against another team's repo (each repo has its own config).
Don't Use When
- Pushing or pulling the n8n workflow JSON itself, use
tools/n8n/pull.sh/push.sh. - Hand-editing generated raw snippets, edit the source doc and re-run
ingestinstead (the parser prunes and overwrites them). - Plain markdown authoring with no extraction, drift, or composition need.
Workflow
Forward (doc to raw snippets to cleaned blocks):
- Locate or create
config.yaml(start a new one fromassets/config.template.yaml); confirmroot,destination,working_dir. - For each source set exactly one of
url:/file:, adescription, and an optionaldoc:; group related sources by a sharedname. python scripts/parser.py ingest --dry-run— review the planned files.python scripts/parser.py ingest— materializes raws (and a verbatim copy of each source doc) toworking_dir, and rewritesstatus.python scripts/parser.py check— gate on exit 2 (drift) before committing.- Clean a source (your judgement, not the parser): pick a source by its unique
name; itsdescriptionis your guidance — what to produce and what to skip (e.g. read-only triage collection, not destructive PITR). Read its rawworking_dir/<group>/<name>_*files and writesnippet-*tosnippets/<group>/perreferences/CLEANING.md— individually runnable, parameterized, read-only and idempotent (harvest inline code from the.mdtoo); run the linter (shellcheckfor sh); skip a raw snippet that cannot be made safe and idempotent (add it to the group'sskippedlist with a reason instatus); list what you produce under the group'scleaned. Also producevalidator-*checks over the collected data (validation separate from fetching; seereferences/CLEANING.md). python scripts/parser.py seal --source <name>records the content seal (the source doc plus your cleaned snippets, comments excluded) intostatus— the baselinecheckuses to flag later snippet-code edits for backport.- Commit
snippets/andconfig.yaml.
Reverse (back-port a fix — check/diff flag it via the seal):
- A dev edits a
snippet-*;checkreportsSNIPPET CODE DRIFT(the seal no longer matches). python scripts/parser.py diff --source <name>shows the change against the seal snapshot; read it with the source doc and propose the doc edit carrying the substantive fix back (cleaned -> raw is lossy, so this is judgement, not a mechanical reverse).- Apply it to the source doc/wiki, then re-
ingestand re-seal. Seereferences/BACKPORT.md.
Rules
- Always run
ingestthencheckbefore committing; never commit withcheckat exit 2. - Always set exactly one of
url:/file:per source (Why: a source with neither is a placeholder that yields no snippets). - Never hand-edit raw snippets or the parser-owned
statusfields — they are regenerated (Why:ingestprunes and overwrites them). - Never promote a mutating or non-idempotent snippet; drop it and record why (Why: cleaned blocks must be safe to re-run in any order an n8n workflow composes them).
- Clean a snippet to ONLY the product(s) its source's
descriptionnames; never assume another product's labels/CRDs/pods (e.g. a general Postgres snippet must not hardcode a Crunchy operator label). Parameterize the target instead — e.g.POD/SELECTORfor pod selection — so the caller supplies it; the workflow that knows the product passes the value. Depend on a second product only when that source'sdescriptiondocuments it (Why: otherwise snippets bleed across products — a "postgres" snippet silently needs Crunchy). - Account for inline code (backtick spans) in the
.mdsnippets when cleaning, not just fenced blocks. - Prefer processable output; use JSON via
jqfor verbose or structured data, plain stdout otherwise — use judgement. - Keep logic in readable bash, not big jq strings: use
jqto extract values and assemble the final JSON (jq -n), and branch / build messages in plain bashif/else(or SQL for queries). Easier to read and debug. - Run the language-appropriate linter on every cleaned snippet you produce.
- Start cleaned snippets from
assets/snippet-template.sh/assets/report-template.md(the standard header); name themsnippet<sep>…where<sep>matches the rest of the filename's style (snippet-for kebab,snippet_for snake); keep each under 100 lines (ideally 30–60). - Every snippet uses Unix (LF) line endings — no carriage returns; strip with
tr -d '\r'if your editor adds them (the parser writes LF). - After cleaning a source, run
sealto record its baseline;checkthen flags a later edit to a cleaned command (not comments) or the source doc as snippet-code drift to backport. - A hand-authored snippet (one you wrote, not produced from a raw) is brought in with
include --source <name> --snippet <file>— it records the snippet under that source'scleanedand seals. For a snippet with no source doc, target a placeholder source (no url/file); the seal then covers the snippet alone. - A
validator-<kebab>.shchecks collection output, not the cluster: it reads the merged JSON on stdin, tolerates missing data (status: "skip"), and emits{check, status, message, docs, sources}; the report shows it by its section. Extract withjq, decide with plain bash (per the rule above). Keep validation separate from fetching; start fromassets/validator-template.sh. - Validators link official docs via the shim, never a hardcoded URL: resolve with
doc_url <aspect> <version>(from the group's sibling shim) and list the object(s) checked insources. To change a link, editconfig.yamlstatus.docs.topics[], runscripts/doc_links.pyto prune dead URLs, thenscripts/doc-link.shto regenerate and commitsnippets/<group>/<group>.sh(Why: URLs stay maintainable and travel with the snippets, resolving with no yq/config at run time). - Markdown report snippets list their expected input keys in the header, and guard each data
section with
{{#if}}so they degrade gracefully when inputs are absent;skippedreasons are one sentence. - Back-port by proposing a doc edit from the
diffoutput (cleaned -> raw is lossy; there is no automatic reverse); keep the doc canonical, never edit the doc and the snippet separately, and re-sealafter. - Keep the
working_dirfetch cache out of commits. - The parser needs
pip install pyyaml markdown-it-py(runs locally or in CI, not in n8n).
Examples
- "Pull the crunchy DR commands from the wiki into snippets" then set the source
url:and runingest. - "Has our runbook drifted from the docs?" then run
checkand read the unified diff. - "Add the postgres triage snippets to the same group" then add a same-
namesource and re-runingest(auto-merged). - "Clean the crunchy-dr-wiki source" then read its
description+ rawworking_dir/…files, write read-onlysnippet-*blocks tosnippets/crunchy-triage/, skip the destructive ones (into the group'sskippedlist), and runshellcheck. - "I fixed the pod selector in the snippet, update the doc" then
diffshows the change; propose the doc edit, apply it, and re-ingest/seal. - "Show three chained results to the user" then write a markdown
snippet-*with{{ .json.path }}and render it withrender.sh.
Edge Cases
- Source URL unreachable then the parser falls back to the
working_dircache with a warning. - A
doc:mirror that does not exist thencheckjust notes it (no error); the seal already covers doc drift, so the mirror is optional. - Same
nameacross sources then snippets merge deterministically in config order. - A source with neither
urlnorfilethen it is a placeholder contributing nothing. - A null timestamp then the drift check still runs (null never suppresses it).
- Back-porting into a
url:source then the agent proposes the doc edit and you paste it into the wiki by hand (a remote wiki cannot be pushed to). - A cleaned command edited after sealing then
checkreportsSNIPPET CODE DRIFT(a comment-only edit does not); rundiff, propose the doc edit, then re-seal.
References
references/CONFIG.md— everyspec/statusfield, the merge rule, and the drift round-trip model.references/PARSER_USAGE.md— CLI, subcommands, exit codes, cache, URL normalization, and a no-network smoke test.references/CLEANING.md— the cleaned contract: safety/idempotency/parameterization rules, inline-code harvest, linters, and therender.shplaceholder syntax.references/BACKPORT.md— the back-port flow:diffsurfaces a dev's cleaned change and the agent proposes the doc edit.assets/config.template.yaml— a baseRunbookParserconfig.yaml(annotatedspec, nostatus) to copy to the tree root and fill when starting a new parser. Schema:references/CONFIG.md.assets/snippet-template.sh/assets/report-template.md— the standard header for cleaned bash / markdown report snippets.assets/validator-template.sh— the standard validator: a read-only check over collected JSON that emits an ok/warn/error/skip verdict with a docs link and the objects it checked.scripts/doc_links.py— validate + prune the doc-link registry (status.docs.topics), following redirects; removes only URLs that are gone (404/410).scripts/doc-link.sh+assets/doc-urls.template.sh— generate each group's static doc-link shimsnippets/<group>/<group>.sh(theaspect|version -> urltable) from the registry; a group's validators source it (a sibling), so URLs resolve with no yq/config at run time.tools/n8n/utils.sh— standard shell messaging (info/warn/err/die/have) sourced by validators and the n8n helpers.references/SKILL_SPEC.md— the SKILL.md manifest spec this file conforms to.