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
01Five 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.
- 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. - 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.
- 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
02A 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.
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" = {}
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" = {}
import type { Reflex } from "./reflex.d.ts" export default (async () => ({ text: "writes landing on the new primary", data: { landing: "yes" }, })) satisfies Reflex
// The same runbook, in code. Five reflexes, and the playbook that lists them. import { load, playbook, reflex } from "@evoke-build/evoke" import { jev } from "@evoke-build/evoke/jev" import { done, handlers, yes } from "../terminal.ts" const drain = reflex({ description: "Drain traffic from the primary database.\n" + "New connections go to the replica; open ones finish.", not_for: ["promoting the replica", "checking writes"], effect: "write", confirm: "Drain the primary?", examples: { "drain connections off the primary": {} }, }, async () => "primary drained") const failover = reflex({ description: "Promote the replica to primary.\n" + "The old primary is demoted; it cannot be undone from here.", not_for: ["draining connections", "checking writes"], effect: "destructive", confirm: "Promote the replica now?", examples: { "fail over to the replica": {} }, }, async () => "replica promoted") const verify = reflex({ description: "Check that writes land on the current primary.\nAnswers yes or no.", effect: "read", confirm: "Check writes?", 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": {}, }, }, async () => ({ text: "writes landing on the new primary", data: { landing: "yes" } })) const tell = reflex({ description: "Tell the ops channel where things stand.", effect: "write", confirm: "Tell the ops channel?", examples: { "tell the ops channel the failover is done": {} }, }, async () => "told #ops") const failback = reflex({ description: "Fail back to the old primary.\n" + "The replica is demoted again; it cannot be undone from here.", effect: "destructive", confirm: "Fail back to the old primary?", examples: { "fail back to the old primary": {} }, }, async () => "failed back to the old primary") const runbook = playbook({ description: "Fail the database over to its replica.\n" + "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": {}, }, }) const reflexes = { drain, failover, verify, tell, failback, runbook } const project = await load({ reflexes, adapter: jev() }) const woven = await project.weave(process.argv[2] ?? "", { ...handlers, proceed: plan => { for (const { n, text, when } of plan.steps) console.log(` ${n} ${text}${when ? ` · if ${when.step} yields ${when.field} "${when.is}"` : ""}`) return yes("Run the plan?") }, }) for (const step of woven.steps) for (const round of step.rounds) if (round.result) console.log(round.result.text) done()
// A runbook as a playbook, from files. The plan prints with the value each // branch waits on, one yes covers it, and the failover asks at its turn. import { load } from "@evoke-build/evoke" import { done, handlers, yes } from "../terminal.ts" const runbook = await load({ root: import.meta.dirname }) const woven = await runbook.weave(process.argv[2] ?? "", { ...handlers, proceed: plan => { for (const { n, text, when } of plan.steps) console.log(` ${n} ${text}${when ? ` · if ${when.step} yields ${when.field} "${when.is}"` : ""}`) return yes("Run the plan?") }, }) for (const step of woven.steps) for (const round of step.rounds) if (round.result) console.log(round.result.text) done()
// The two handlers the ops and runbook apps share, and one yes/no question. // A confirm is a question; an ask is a numbered menu, or a blank to type into. import { createInterface } from "node:readline/promises" import type { Handlers } from "@evoke-build/evoke" const { stdin: input, stdout: output } = process const terminal = createInterface({ input, output }) export const yes = async (question: string) => (await terminal.question(`${question} [y]es [n]o > `)).startsWith("y") // Fits a project of any reflexes export const handlers: Handlers<any> = { confirm: d => yes(d.prompt.template), ask: async d => { const given: Record<string, string> = {} for (const { arg, ask, choices } of d.missing) { const keys = choices.type === "options" ? Object.keys(choices.options) : choices.type === "vocab" ? Object.keys(choices.words) : [] const menu = keys.map((key, i) => `[${i + 1}] ${key}`).join(" ") const typed = await terminal.question(`${ask} ${menu}${menu && " "}> `) given[arg] = keys[Number(typed) - 1] ?? typed } return given }, } export const done = () => terminal.close()
- 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. - 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.
- 3The same plan over files or code.
load()takes a directory with itsevoke.toml, or the reflexes and the playbook handed over as objects.
One boundary
03The 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 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
04Run 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.
- Node 24.5 or newer, and the SDKThe plan runs in your app.
- A classifier keyGet a key from TypeSafe AI. The sentence and each step are one request, billed to your key.
- Real programsThe five shown return fixed lines. Yours would call your database, and pass
the
signalthey receive to every call: each decision has 30 seconds. - Recovery, decided by youA step that fails stops the rest. What to do about the steps already done is your application's call.
- A chat botevoke has no chat integration. In an app, the confirm handler is where a bot would ask for its yes.
-
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=modulenpm install @evoke-build/evoke -
Save the two files
Save
runbook.tsfrom the code tab above in~/runbook. Saveterminal.tsfrom the app tab in your home folder, the folder above it, whererunbook.tsimports it from. -
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
nto 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.