Skip to content

Executable Blocks

Real runbooks mix runnable shell, interactive commands, SQL for another client, expected-output samples, configuration to copy, and destructive commands. runsheets therefore executes nothing unless a block opts in.

The convention

The fence's info string is split on whitespace. The first word is the language. Every following word is a flag.

```bash run
aws sts get-caller-identity
```
Info string Kind Behaviour
```bash display Rendered with syntax highlighting and a Copy button. No Run button.
```bash run run Run button. Executed with the run's inputs in the environment; stdout, stderr, exit status and timing recorded.
```bash destructive destructive Run button behind a typed confirmation code that the server issues per block. Implies run. The step shows the blast-radius banner.
```ruby run run Same as bash run, executed with ruby or the runbook's interpreters.ruby.
```bash background background Start and Stop buttons for long-running processes such as a tunnel or a log tail. No timeout. Output streams into the page; the process is listed in the sidebar's Running panel on every page; anything still running is stopped when the run ends and recorded as stopped, not as a failure.
```bash terminal terminal No Run button. Labelled "run this in your own terminal, then confirm". For anything interactive: read -s, an SSO login that opens a browser, an interactive psql session. An I ran this button records the operator's confirmation, with an optional note.
```text expect expect Never executed. Linked to the nearest executable block above it and compared with that block's real output once it has run: a match says "matches expected", a mismatch shows both side by side with the differences marked. {{any}} matches anything within a line. A hint for the operator; it does not gate pass or fail.

Flags are additive where it makes sense: bash run destructive and bash destructive are the same thing.

Expect blocks

An expect block illustrates the executable block nearest above it in the same document, whatever prose sits between them. Its toolbar says which block it belongs to and links to it. An expect block with no executable block above it is just displayed.

When that block has run, its output is compared with the expected text line by line, ignoring trailing whitespace, and the expect block is hidden, since the result now shows what it said:

  • A match shows the output alone, with ✓ matches expected in the footer.
  • A mismatch shows the output and the expected text in two panes, titled Output and Expected. Lines do not wrap, so each line sits beside the line it is compared with. Rows that differ are tinted and the characters that differ are highlighted. A line only one side has faces a hatched gap on the other. The footer says differs from expected.

Output often holds something that changes on every run: a timestamp, an id, a PID, a duration. Write {{any}} in its place. It matches any characters, including none, within one line, so this expect block matches whatever the run id is:

```text expect
Running block 010-say-hello-1 of step 010-say-hello in run {{any}}
```

{{any}} does not match across lines, and a line that is only {{any}} matches exactly one line of output. In the side-by-side view each {{any}} is shown as a tag.

Background blocks

A background block is for something that must stay up while later steps run. It has no timeout, so the step's timeout does not apply to it. Stop it from its own Stop button, from the Running panel on any page (including the pages of another runbook), or by ending the session. Switching to another runbook leaves it running. The process group gets TERM, then KILL two seconds later if needed, and the execution is recorded with state stopped. A background process that exits on its own is recorded like a run block: finished with its exit status, which counts as a failure if non-zero.

Which languages execute

Only languages with an interpreter can execute. The defaults are bash, sh, zsh and ruby. A block in any other language that asks for run renders with the warning "sql blocks cannot execute; displayed only (map it under interpreters in the runbook front matter)" and no Run button, so a mistake is visible rather than silent.

Interpreters

The runbook's front matter can add or override entries, and a language it maps executes like any other:

interpreters:
  ruby: bin/rails runner -
  sql: psql -X -v ON_ERROR_STOP=1 -f
  python: python3

When a block executes, its code is written to a file in the run directory and the interpreter command is run with that path appended. With the map above a ruby run block becomes:

bin/rails runner - <run-dir>/blocks/020-x-1.1.cmd

and a sql run block becomes psql -X -v ON_ERROR_STOP=1 -f <path>, with psql taking its connection from the PGHOST, PGUSER, PGPASSWORD and PGDATABASE inputs in the environment. examples/single_md_file_runbooks/db-maintenance.md is a complete runbook built this way.

The command is split with Shellwords, so quoting works as it would in a shell, but there is no shell: a $VARIABLE in the interpreter command is passed literally. If you need expansion there, go through bash -c and take the file as $0:

interpreters:
  sql: bash -c 'psql "$DATABASE_URL" -X -v ON_ERROR_STOP=1 -f "$0"'

Because every block is a fresh process, a mapping like bin/rails runner - boots Rails for every Ruby block. That is the right trade for correctness in a runbook; keep Ruby blocks few and meaningful.

Rules

  • Unknown flags are an authoring error. They render with a visible warning in the block's toolbar and are listed by runsheets --check. They are never silently ignored.
  • Conflicting flags warn. bash background terminal picks the first and warns. destructive always wins because it is the safer reading. run beside background or destructive adds nothing and is not warned about, in either order: bash run background and bash background run are both background. run beside terminal or expect contradicts it, so text expect run is an expect block, with a warning.
  • The block's code is exactly the fence's contents. Leading indentation inside a list item is removed, as GitHub does. Nothing is prepended: no set -e, no shebang. If you want bash to stop at the first failure, say so in the block.
  • Nothing carries over between blocks. Each execution is a fresh process. See Execution Model.

Block identity

Every fenced block with an info string gets an id of the form <step-slug>-<n>, numbered from 1 in document order, including blocks nested in lists. Display-only blocks are numbered too, so adding a flag to an existing block does not renumber its neighbours. Bare fences with no language at all have no identity.

Ids appear in the page (data-block), in the run record, and in the names of the .cmd and .out files.

Blocks in the preamble, verify and rollback

The convention applies to every document of a runbook. A plain markdown file the runbook links to is rendered with every block display only. The preamble in runbook.md uses the id prefix runbook; verify.md and rollback.md use verify and rollback. A bash run block in rollback.md is executable from its page and from the sidebar panel's link, which is often exactly what you want.

Why the info string, not a comment or attribute

GitHub, editors and other renderers display bash run as bash, so a runbook written for runsheets degrades to a perfectly ordinary markdown file everywhere else. The flags live in the one place every markdown tool already preserves.

A parser detail you will never see

kramdown's GFM parser only accepts a single word as a fence info string; "``bash run" is not even a code block to it. runsheets rewrites the info string to kramdown'slang?opts` form before parsing and wraps the block afterwards. The markdown on disk is never changed.