evoke

Examples · 05 of 06

A runbook whose check picks the next step. Only the destructive one waits for a yes.

A runbook that waits for a yes before the step you cannot undo. A reflex is a small program you install and ask for in your words. Here the runbook is a playbook: five sentences in one plan, each decided as if you had typed it. The check yields whether writes land, and the two steps after it each name the value they run under. The database programs shown are stand-ins that return fixed lines.

The request and the result

01

Five steps run from one typed sentence.

The person types the sentence. The plan prints with the value each branch waits on, one yes covers it, and the check's result picks the step that follows. Recorded on 29 September 2026 against Jev, TypeSafe AI's model, which reads each sentence.

~/runbook
$ node runbook.ts "switch the database to the replica" 1 drain connections off the primary 2 promote the replica 3 check whether writes land on the new primary 4 tell the ops channel the failover is done · if 3 yields landing "yes" 5 fail back to the old primary · if 3 yields landing "no" Run the plan? [y]es [n]o > y Promote the replica now? [y]es [n]o > y primary drained replica promoted writes landing on the new primary told #ops
  1. 1The author listed both branches. The check yields landing, and the two steps after it each name the value they run under. The one not chosen is skipped, and the plan says so.
  2. 2Every step is decided on its own. Each sentence of the plan is routed to a reflex as if you had typed it, and gated by its effect: the drain changes something, the check only reads.
  3. 3Only the failover asks at its turn. It cannot be undone, so its author marked it destructive. It asks every time, whatever the yes over the whole plan said.

The operations

02

A playbook and its reflexes.

The runbook is a manifest whose body is a plan: steps, one sentence each, two of them under when. Each step is decided over the reflexes installed beside it. No loop in your app: the plan is the author's, and the result picks among the steps it lists.

runbook/reflex.tomlthe author's
reflex = 1

description = """
Fail the database over to its replica.
Drains the primary, promotes the replica and checks that writes land on it; tells the ops channel when they do, fails back when they do not."""
not_for = ["draining the primary on its own", "checking writes on their own"]
effect  = "destructive"
confirm = "Run the failover runbook?"
steps = [
  "drain connections off the primary",
  "promote the replica",
  "check whether writes land on the new primary",
  { say = "tell the ops channel the failover is done", when = { landing = "yes" } },
  { say = "fail back to the old primary",              when = { landing = "no" } },
]

[examples]
"run the failover runbook"           = {}
"switch the database to the replica" = {}
"the primary database is failing"    = {}
verify/reflex.tomla stand-in
reflex = 1

description = """
Check that writes land on the current primary.
Answers yes or no."""
effect  = "read"
confirm = "Check writes?"
run     = "verify.mts"

[yields]
landing = "quoted"

[examples]
"check whether writes land on the new primary" = {}
"are writes landing on the primary"            = {}
"see whether writes reach the new primary"     = {}
verify/verify.mtsa stand-in
import type { Reflex } from "./reflex.d.ts"

export default (async () => ({
  text: "writes landing on the new primary",
  data: { landing: "yes" },
})) satisfies Reflex
  1. 1The check yields a field. [yields] landing = "quoted" is the author's promise about its result. A branch waits on a field the step before it yields, a text or a number.
  2. 2A result never writes a step. The list is closed and printed before anything runs. A value no step lists picks nothing, and the plan goes on.
  3. 3The same plan over files or code. load() takes a directory with its evoke.toml, or the reflexes and the playbook handed over as objects.

One boundary

03

The destructive step waits for a yes.

The failover cannot be undone from where it runs, so its author marked it destructive. Answer no, and the plan ends there. The drain stays done, and the check and both branches never run.

node
$ node runbook.ts "switch the database to the replica"
  1  drain connections off the primary
  2  promote the replica
  3  check whether writes land on the new primary
  4  tell the ops channel the failover is done · if 3 yields landing "yes"
  5  fail back to the old primary · if 3 yields landing "no"
Run the plan?  [y]es [n]o > y
Promote the replica now?  [y]es [n]o > n
primary drained

The question is the one the failover's author wrote. Answer no, and the handler returns false: weave() ends the plan after that step, the rest read skipped, and the drain's result is all that prints. weave(), in the manual →

Set it up

04

Run the runbook from one file, with the SDK.

runbook.ts holds the five reflexes and the playbook, so it runs on its own. Its programs are stand-ins that change nothing, so it is safe to run.

  1. Make the project

    A folder for the app, with a package of ES modules and the SDK.

    mkdir -p ~/runbook && cd ~/runbook && npm init -y && npm pkg set type=module
    npm install @evoke-build/evoke
  2. Save the two files

    Save runbook.ts from the code tab above in ~/runbook. Save terminal.ts from the app tab in your home folder, the folder above it, where runbook.ts imports it from.

  3. Start the runbook

    The plan prints and asks for its yes. The drain runs, the failover asks for its own, and the check picks between the two steps after it. Answer n to the failover, and the plan stops there.

    export TYPESAFE_API_KEY=<value>
    node runbook.ts "switch the database to the replica"

To make it yours, replace each stand-in with a program that calls your database. Keep the check's landing, which the two branches wait on.