Directory Structure¶
A runbook is a directory, or a single file. The directory is the primary shape. Its name is the runbook's slug, which names the run record directory and appears in the page header.
staging-teardown/
runbook.md front matter + preamble (required)
steps/
010-confirm-nothing-to-preserve.md
020-stop-the-pipeline.md
030-drain-ecs-services.md
verify.md whole-procedure verification (optional)
rollback.md what to do when it fails partway (optional)
assets/ images, SVG diagrams referenced by the markdown
runbook.md¶
Required. Its front matter carries what the operator needs before the first step; the body is the preamble, rendered on the landing page below the step list. See Front Matter Reference for every key.
Fenced blocks in the preamble follow the same convention as everywhere else,
so a preamble can carry an executable block. Their ids are prefixed
runbook-.
steps/¶
Each markdown file is one step. Steps are ordered by filename using a
numeric-aware sort: the leading digits are compared as numbers, then the
rest of the name as text. 010-, 020-, 030- with gaps leaves room to
insert a step later without renumbering. A file without a numeric prefix
sorts before the numbered ones (it compares as zero), so give every step a
number.
The step slug is the filename without its extension, for example
020-stop-the-pipeline. It is used in URLs (/steps/020-stop-the-pipeline),
in block ids (020-stop-the-pipeline-1), and in the run record. Slugs must
be unique; a duplicate is reported as an authoring warning. So is a step
file named runbook.md, verify.md or rollback.md inside steps/: it
has the name of a special document, so give it a number.
The step number shown in the page is the leading digits of the slug, or the step's position when there are none.
Each step has optional front matter and a body. The body is ordinary markdown: the instruction, the command, and the "how to tell it worked" line that every good step carries.
---
title: Drain the ECS services
kind: automated
timeout: 600
---
Scale every service in the cluster to zero and wait for the tasks to stop.
```bash run
aws ecs update-service --cluster "$CLUSTER" --service web --desired-count 0
aws ecs wait services-stable --cluster "$CLUSTER" --services web
It worked when the wait returns without error and the console shows 0 running tasks.
A step's title comes from its front matter, else from the first `# Heading`
in the body's prose, else from the slug with its number stripped, dashes
and underscores turned into spaces, and only the first letter capitalized
(`020-stop-the-pipeline` becomes "Stop the pipeline"). A `# comment` line inside a fenced code block is never
taken for the title.
## `verify.md` and `rollback.md`
Both optional, both ordinary step documents with front matter and a body.
They are not numbered steps and never appear in the prev/next sequence, but
they are listed in the sidebar under **Also** and are served at
`/steps/verify` and `/steps/rollback`.
`verify.md` holds whole-procedure checks. Its executable blocks run like any
other during a run, and the **Checks** page gathers `verify.md` and the
verify-kind steps on one page, with a **Run all** button that runs just
those checks inside the runbook's run.
`rollback.md` additionally renders inside a collapsible **Rollback** panel in
the sidebar of every step page, so it is one click away when something goes
wrong and never needs scrolling to find.
## `assets/`
Not special to runsheets, just the conventional place for images. Any file
under the directory `runsheets` was started on can be referenced with a relative link and is
served through `/files/`:
```markdown

Relative src and href values are resolved against the markdown file's
own directory, so from steps/020-x.md the path above resolves to
<runbook>/assets/topology.svg. Started on a library, links may reach
anywhere in the library; started on one runbook, only within it. A path
that climbs out is left untouched and will 404. Hidden entries (names starting
with .) are never served, which keeps .git and .env out of reach.
Images (.png, .jpg, .jpeg, .gif, .webp, .svg), PDFs and .txt
files open in the browser. Any other file is sent as a download, so an
HTML or XML file linked from a runbook is saved rather than shown, and
nothing served from /files/ runs script. See
Security Posture.
A link to a markdown file is the exception. It goes to /docs/, which
renders the file as a page: a link to one of the runbook's own files (a
step, verify.md, rollback.md, runbook.md) opens that step's page, a
link to another runbook in the library opens that runbook's entry, and
any other markdown file is shown, in a new tab, as a plain document with
nothing executable. See Runbooks and documents.
Single-file runbooks¶
An existing runbook is usually one markdown file with a heading per step. runsheets reads that shape directly: pass the file instead of a directory.
---
title: Monthly PostgreSQL maintenance
inputs:
- name: PGDATABASE
default: app
interpreters:
sql: psql -X -v ON_ERROR_STOP=1 -f
---
# Monthly PostgreSQL maintenance
Everything above the first `##` heading is the preamble.
## Check the connection
<!-- kind: verify, timeout: 30 -->
```sql run
select current_database(), current_user;
```
## Vacuum the worst table
<!-- kind: automated, timeout: 1800 -->
```sql run
vacuum (verbose, analyze) events;
```
## Verify
Whole-procedure checks, as verify.md would hold.
## Rollback
What to do when it fails partway, as rollback.md would hold.
The rules:
- The front matter is the runbook's, with the same keys as
runbook.md, and must have atitle. Without one the file is not a runbook: passed torunsheetsit fails to load, and in a library it is a plain document. - Every
##heading starts a step, in document order. Headings inside fenced code blocks are ignored. Deeper headings (###) belong to the step they sit under. - The heading text is the step's title. A closing run of
#s after a space is dropped, as in markdown (## Rollback ##), but a#that is part of the text stays:## Upgrade to C#is titled "Upgrade to C#". - A code fence opened in a section and never closed swallows every heading after it, so it is reported as an authoring warning on that section.
- A step's attributes (
kind,timeout,destructive,cwd, andtitleto override the heading) go in an HTML comment on the first non-blank line after the heading, written as a YAML flow mapping:<!-- kind: verify, timeout: 30 -->. GitHub and every other renderer hide it. A comment that does not start withkey:is ordinary prose and is left alone. - A section headed Verify or Rollback (any case) plays the part of
verify.mdorrollback.md. A section with any other heading can take the role with<!-- role: verify -->. - Step slugs are generated from the position and the heading,
010-check-the-connection,020-vacuum-the-worst-table, so block ids and run record files look exactly as they do for a directory runbook. Inserting a heading renumbers the ones after it; the run record keeps the slugs that were current when it ran, and the drift view shows the difference. - The slug is the file name without
.md, or the directory name when the file is calledrunbook.md. The working directory for blocks is the file's directory.
runsheets --init name.md writes a starter file in this shape. The bundled
examples/single_md_file_runbooks/db-maintenance.md is a complete one.
Several runbooks in one directory¶
Started on a directory that is not itself a runbook, runsheets offers
every runbook beneath it in a folder tree (see
Choosing a runbook). A
runbook's slug there is its path inside the directory, without .md, so
two runbooks in one folder cannot share one. deploy.md beside
deploy/runbook.md, or backup.md beside a folder backup/, is a
clash: the folder, or the first runbook in name order, keeps the name and
the other is shown as a broken entry saying "another runbook or a folder
here already has the name …". Rename one.
What runsheets does not read¶
Anything else in the directory is ignored: a README.md, a Gemfile, a
scripts/ folder. Steps live only in steps/, and only .md files there
count.
Run records live elsewhere¶
runsheets never writes inside a runbook directory. Run records go to
~/.local/share/runsheets/runs/<slug>/<timestamp>/ by default; see
The Run Record. This keeps the runbook directory
clean and committable, and keeps captured output, which may contain
account-scoped data, out of the docs repository.
runsheets re-reads the runbook whenever one of its markdown files changes, so you can edit steps with the server up and see them on the next page load.