Six sentences, at work.
Each one becomes a call of a small program, checked before it runs. The same recipe as a file anyone can install, or as code your app keeps. Every scene is a real session.
An ops channel
01Scale, silence, roll back.
Three recipes for one channel. The one that wraps kubectl is a manifest and nothing
else. A write runs when the bar is met. A rollback always asks.
reflex = 1 description = """ Scale a service to a number of replicas. One deployment in one environment; the number is set, never added to.""" not_for = [ "restarting or rolling back a service", "resizing a database or a disk", "autoscaling rules", ] tags = ["ops"] effect = "write" confirm = "Scale {service} in {env} to {replicas}?" run = [ "kubectl", "-n", "{env}", "scale", "deployment", "{service}", "--replicas", "{replicas}", ] [args.service] ask = "Which service?" vocab = "services" [args.env] ask = "Which environment?" vocab = "envs" [args.replicas] ask = "How many replicas?" pick = "number" range = [1, 20] [examples] "scale checkout to 6 in staging" = { replicas = "6" } "run 3 replicas of search in prod" = { replicas = "3" } "take payments down to 1 replica" = { replicas = "1" } [tests] "bring checkout up to 12 in prod" = { replicas = "12" } "scale checkout" = { replicas = false } "restart checkout" = false
checkout = { what = "The checkout API.", value = "checkout-api" } search = { what = "The search API.", value = "search-api" } payments = { what = "Payments; also 'the gateway'.", value = "payments-svc" }
import { execFile } from "node:child_process" import { promisify } from "node:util" import { type Vocab, reflex } from "@evoke-build/evoke" const kubectl = async (...args: string[]) => (await promisify(execFile)("kubectl", args)).stdout.trim() export const scale = reflex({ description: "Scale a service to a number of replicas.\n" + "One deployment in one environment; the number is set, never added to.", not_for: [ "restarting or rolling back a service", "resizing a database or a disk", "autoscaling rules", ], effect: "write", confirm: "Scale {service} in {env} to {replicas}?", args: { service: { ask: "Which service?", vocab: "services" }, env: { ask: "Which environment?", vocab: "envs" }, replicas: { ask: "How many replicas?", pick: "number", range: [1, 20] }, }, examples: { "scale checkout to 6 in staging": { replicas: "6" }, "run 3 replicas of search in prod": { replicas: "3" }, "take payments down to 1 replica": { replicas: "1" }, }, }, ({ service, env, replicas }) => kubectl("-n", env, "scale", "deployment", service, `--replicas=${replicas}`)) // silence and rollback follow, the same way // The team's names, in the file's form; from a database in a real app. export const vocab: Vocab = { services: { checkout: { what: "The checkout API.", value: "checkout-api" }, search: { what: "The search API.", value: "search-api" }, payments: { what: "Payments; also 'the gateway'.", value: "payments-svc" }, }, envs: { staging: "The pre-production cluster.", prod: { what: "Production; also 'live'.", value: "production" }, }, hosts: { "db-3": "The primary Postgres host.", "web-1": "The first web node.", }, }
// The project next to this file: reflex.toml, vocab/, overlays/. // One sentence in, one line out. import { load } from "@evoke-build/evoke" import { done, handlers } from "../terminal.ts" const ops = await load({ root: import.meta.dirname }) const handled = await ops.handle(process.argv[2] ?? "", handlers) console.log(handled.outcome === "ran" ? handled.result.text : handled.outcome) done()
// The reflexes above, loaded from code alone: no files, // the team's names handed over with `with`. import { load } from "@evoke-build/evoke" import { jev } from "@evoke-build/evoke/jev" import { done, handlers } from "../terminal.ts" import { rollback, scale, silence, vocab } from "./reflexes.ts" const reflexes = { scale, silence, rollback } const ops = (await load({ reflexes, adapter: jev() })).with({ vocab }) const handled = await ops.handle(process.argv[2] ?? "", handlers) console.log(handled.outcome === "ran" ? handled.result.text : handled.outcome) done()
- 1kubectl, no code. An argv recipe is a manifest and nothing else.
- 2Your names, from a vocabulary. The value is what the program gets. Jev sees the word.
- 3In code, the same text.
withhands the names over, from a database in a real app.
An inbox
02Lines in, decisions out.
The same three recipes, fed a file. What is sure runs. What needs a person is queued with its ranking. What is nobody's stays untouched.
scale checkout to 6 in staging silence db-3 what is the weather like
// The inbox as a stream. What is sure runs; the rest is queued for a person, // with the ranking or the missing pieces. import { createInterface } from "node:readline" import { load } from "@evoke-build/evoke" const ops = await load({ root: import.meta.dirname }) for await (const line of createInterface({ input: process.stdin })) { const d = await ops.decide(line) if (d.outcome === "run") console.log((await ops.run(d)).text) else console.log(`queued · ${d.outcome} · "${line}"`) }
- 1At the terminal, exit 3 is the queue.
evoke < inbox.txtnames the command a person should run. - 2The ranking travels with it. Every decision carries its contenders and what is missing.
- 3Nothing was invented. A question about the weather is nobody's, and stays so.
A payments desk
03The maker types. The checker says yes.
A payee is one of your words, never invented. Paying is destructive, so the decision waits as plain data until a second person confirms it.
reflex = 1 description = """ Pay an approved payee from one of our accounts. One transfer, released after a second person confirms it.""" not_for = [ "adding or changing a payee", "moving money between our own accounts", "asking a balance", ] tags = ["treasury"] effect = "destructive" confirm = "Pay {payee} {amount} from {account}?" run = "pay.mts" [config] api = "The payments API address" token = { about = "The payments API token", secret = true } [args.payee] ask = "Which payee?" vocab = "payees" [args.account] ask = "From which account?" vocab = "accounts" [args.amount] ask = "How much?" pick = "number" range = [1, 250000] [args.ref] ask = "What reference?" pick = "quoted" optional = true [examples] 'pay Acme 12400 from ops, ref "8812"' = { amount = "12400", ref = "8812" } "send 950 to the cleaners from the office account" = { amount = "950" } "wire 3200 to Acme from treasury" = { amount = "3200" } [tests] 'settle 18000 with Acme, ref "Q3"' = { amount = "18000", ref = "Q3" } "pay Acme from ops" = { amount = false } "add Acme as a new payee" = false
acme = { what = "Acme, the packaging supplier.", value = "py_acme" } cleaners = { what = "Blitz, the office cleaners.", value = "py_blitz" }
import { load, reflex } from "@evoke-build/evoke" import { jev } from "@evoke-build/evoke/jev" const pay = reflex({ description: "Pay an approved payee from one of our accounts.\n" + "One transfer, released after a second person confirms it.", not_for: [ "adding or changing a payee", "moving money between our own accounts", "asking a balance", ], effect: "destructive", confirm: "Pay {payee} {amount} from {account}?", args: { payee: { ask: "Which payee?", vocab: "payees" }, account: { ask: "From which account?", vocab: "accounts" }, amount: { ask: "How much?", pick: "number", range: [1, 250000] }, ref: { ask: "What reference?", pick: "quoted", optional: true }, }, examples: { 'pay Acme 12400 from ops, ref "8812"': { amount: "12400", ref: "8812" }, "send 950 to the cleaners from the office account": { amount: "950" }, }, }, async ({ payee, account, amount, ref }, { signal }) => { const response = await fetch(`${process.env.PAYMENTS_API}/transfers`, { method: "POST", headers: { authorization: `Bearer ${process.env.PAYMENTS_TOKEN}`, "content-type": "application/json", }, body: JSON.stringify({ from: account, to: payee, amount, reference: ref }), signal, }) if (!response.ok) throw new Error(`payments answered ${response.status}`) const { id } = (await response.json()) as { id: string } return { text: `paid ${amount} from ${account}, transfer ${id}`, data: { id } } }) // This desk's payees, from the database; nothing else is a word. const desk = (await load({ reflexes: { pay }, adapter: jev() })).with({ vocab: { payees: { acme: { what: "Acme, the packaging supplier.", value: "py_acme" }, cleaners: { what: "Blitz, the office cleaners.", value: "py_blitz" }, }, accounts: { ops: { what: "The operations account.", value: "acc_ops" }, office: { what: "The office account.", value: "acc_office" }, }, }, }) // then the maker and the checker, line for line as in checker.ts
// The desk decides; the decision waits in a queue as plain data; // a second person reads the call and runs it. import { load } from "@evoke-build/evoke" const desk = await load({ root: import.meta.dirname }) // maker const decided = await desk.decide('pay Acme 12400 from ops, ref "invoice 8812"') if (decided.outcome !== "confirm") throw new Error(decided.outcome) // through the queue and back: a decision is plain data const queued: typeof decided = JSON.parse(JSON.stringify(decided)) // checker: the call, then the effect, the weakest judgment and why it stopped console.log(queued.call) console.log(queued.prompt.own) console.log((await desk.run(queued, { confirmed: true })).text)
- 1A payee is a word. Its value is an id in the payments system. Jev never sees it.
- 2The decision is plain data. It waits in any queue, and only
confirmed: trueruns it. - 3Each desk, its own words.
withcompiles the desk's payees in milliseconds.
A plant floor
04Stop the filler.
A line is a word whose value is the controller's id. The night shift's phrases live in an overlay the plant owns, and the shipped recipe never changes.
reflex = 1 description = """ Stop a production line. A controlled stop at the end of the cycle; restart at the panel.""" not_for = ["an emergency stop", "pausing one machine", "holding a work order"] tags = ["floor"] effect = "destructive" confirm = "Stop {line}?" run = "stop.mts" [config] gateway = "The line controller gateway address" [args.line] ask = "Which line?" vocab = "lines" [examples] "stop line 3" = {} "shut the packaging line down" = {} "bring line 2 to a stop" = {} [tests] "halt line 3 after this cycle" = {} "emergency stop" = false "hold work order 4471" = false
"line 2" = { what = "Line 2, labelling; also 'the labeller'.", value = "L2" } "line 3" = { what = "Line 3, bottling; also 'the filler'.", value = "L3" } "packaging" = { what = "The packaging line, end of the hall.", value = "L5" }
# The night shift's words. The shipped recipe is untouched. confirm = "Stop {line} at the end of this cycle?" [examples] "kill the filler" = { line = "line 3" } "take the labeller down" = { line = "line 2" } "shut packaging at the end of the run" = { line = "packaging" } [tests] "the filler is jammed, stop it" = { line = "line 3" } "e-stop" = false
import { load, reflex } from "@evoke-build/evoke" import { done, handlers } from "../terminal.ts" const stop = reflex({ description: "Stop a production line.\n" + "A controlled stop at the end of the cycle; restart at the panel.", not_for: ["an emergency stop", "pausing one machine", "holding a work order"], effect: "destructive", confirm: "Stop {line}?", args: { line: { ask: "Which line?", vocab: "lines" } }, examples: { "stop line 3": {}, "shut the packaging line down": {}, "bring line 2 to a stop": {}, }, }, async ({ line }, { signal }) => { const response = await fetch(`${process.env.GATEWAY}/lines/${line}/run`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ run: false, when: "end of cycle" }), signal, }) if (!response.ok) throw new Error(`the gateway answered ${response.status}`) return `${line} stops at the end of this cycle` }) if (!response.ok) throw new Error(`the MES answered ${response.status}`) return `work order ${order} on hold: ${reason}` }) // The words are files the floor edits: // words/vocab/lines.toml and words/overlays/stop.toml const plant = await load({ root: `${import.meta.dirname}/words`, reflexes: { stop }, }) const handled = await plant.handle(process.argv[2] ?? "", handlers) console.log(handled.outcome === "ran" ? handled.result.text : handled.outcome) done()
- 1A line is a word. Its value,
L3, is what the gateway gets. - 2The overlay is the plant's. The shipped recipe never changes. The night shift's phrases are one file.
- 3Reflexes in code, words in files. The floor edits
words/, never the code.
A runbook
05Each step names the next.
Three recipes in a chain. Every hop passes the bar, and only the destructive one stops for a person.
reflex = 1 description = """ Drain traffic from the primary database. New connections go to the replica; open ones finish.""" effect = "write" confirm = "Drain the primary?" run = "drain.mts" [examples] "drain the primary" = {}
import type { Reflex } from "./reflex.d.ts" export default (async () => ({ text: "primary drained", data: { next: "fail over to the replica" }, })) satisfies Reflex
// The same runbook, in code. Three reflexes, one loop. import { load, reflex } from "@evoke-build/evoke" import { jev } from "@evoke-build/evoke/jev" import { done, handlers } from "../terminal.ts" const drain = reflex({ description: "Drain traffic from the primary database.\n" + "New connections go to the replica; open ones finish.", effect: "write", confirm: "Drain the primary?", examples: { "drain the primary": {} }, }, async () => ({ text: "primary drained", data: { next: "fail over to the replica" }, })) const failover = reflex({ description: "Promote the replica to primary.\n" + "The old primary is demoted; it cannot be undone from here.", effect: "destructive", confirm: "Promote the replica now?", examples: { "fail over to the replica": {} }, }, async () => ({ text: "replica promoted", data: { next: "check that writes land on the new primary" }, })) const verify = reflex({ description: "Check that writes land on the current primary.", effect: "read", confirm: "Check writes?", examples: { "check that writes land on the new primary": {} }, }, async () => "writes landing on the new primary") const reflexes = { drain, failover, verify } const runbook = await load({ reflexes, adapter: jev() }) let next: string | undefined = process.argv[2] while (next) { const step = await runbook.handle(next, handlers) if (step.outcome !== "ran") break console.log(step.result.text) next = (step.result.data as { next?: string } | undefined)?.next } done()
// A runbook as sentences, from files. Each step's body names the next step // in words; only the destructive one stops for a person. import { load } from "@evoke-build/evoke" import { done, handlers } from "../terminal.ts" const runbook = await load({ root: import.meta.dirname }) let next: string | undefined = process.argv[2] while (next) { const step = await runbook.handle(next, handlers) if (step.outcome !== "ran") break console.log(step.result.text) next = (step.result.data as { next?: string } | undefined)?.next } done()
- 1A step names the next. The program returns
data.next, a sentence. - 2Every hop passes the bar. Drain is a write, verify is a read.
- 3Only the failover asks. A destructive recipe always confirms.
A lesson
06A miss becomes a lesson.
Claude proposes the line, evoke teach checks it at the door, evoke test
keeps it only when nothing regresses, and git is the undo.
// What evoke could not decide becomes wording. A model proposes the lessons, // evoke test disposes, git undoes. import { execFile } from "node:child_process" import { readFile } from "node:fs/promises" import { promisify } from "node:util" import Anthropic from "@anthropic-ai/sdk" import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod" import { z } from "zod" const run = promisify(execFile) // evoke's own lines are on stderr; stdout is a reflex's result const evoke = async (...args: string[]) => (await run("evoke", args)).stderr // A miss: a sentence nobody's, or one that stopped under the bar const logFile = `${process.env.HOME}/.local/state/evoke/log.jsonl` const log = await readFile(logFile, "utf8") const decisions = log.trim().split("\n").map(line => JSON.parse(line)) const missed = (d: { outcome: string; because?: { type: string }[] }) => d.outcome === "abstain" || d.because?.some(b => b.type === "under_floor") const undecided = new Map<string, string>( decisions.filter(missed).map(d => [d.input, d.call ?? "nothing"]), ) const names = (await evoke("show")).trim().split("\n") .map(row => row.trim().split(/\s+/)[0]!) .filter(name => name !== "inactive") const manifests = await Promise.all(names.map(name => evoke("show", name))) const brief = `Below are the reflexes of an evoke project, then sentences it could not settle. For each sentence that can only mean one reflex, write the call it means in evoke's grammar, "name arg=value …": only option keys, vocabulary words and exact spans of the sentence may be values. Leave out a sentence that fits no reflex, or two.` const misses = [...undecided] .map(([input, call]) => `${input}\n evoke proposed: ${call}`) const Lessons = z.object({ lessons: z.array(z.object({ utterance: z.string(), call: z.string() })), }) const { parsed_output } = await new Anthropic().messages.parse({ model: "claude-opus-5", max_tokens: 4000, system: `${brief}\n\n${manifests.join("\n")}`, messages: [{ role: "user", content: misses.join("\n") }], output_config: { format: zodOutputFormat(Lessons) }, }) // A value evoke does not know is refused at the door for (const { utterance, call } of parsed_output?.lessons ?? []) { process.stdout.write(await evoke("teach", utterance, call)) } // Green keeps the lessons; a regression drops them try { process.stdout.write(await evoke("test")) } catch { await run("git", ["checkout", "--", "overlays"]) console.log("a regression: the lessons are undone") }
[examples] "make db-3 shut up for 90 minutes" = { host = "db-3", duration = "90 minutes" }
- 1A miss is a sentence under the bar. From the log, with the call Jev proposed.
- 2Checked at the door.
evoke teachrefuses a value the recipe does not know. - 3Kept only if green.
evoke testjudges every record. A regression is undone with git.
Run it
07Every scene above is a real session.
The two handlers every app shares are twenty lines. Install evoke, add the SDK, and say it yourself.
// The two handlers every app here shares. // 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 }) // Fits a project of any reflexes export const handlers: Handlers<any> = { confirm: async d => { const typed = await terminal.question(`${d.prompt.template} [y]es [n]o > `) return typed.startsWith("y") }, 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()
The CLI
Fetches the release for your machine, checks its digest, and puts evoke in ~/.local/bin.
curl -fsSL https://evoke.build/install.sh | shThe SDK
Node 24 or newer, ES modules, no dependencies. The core ships inside as WebAssembly.
npm install @evoke-build/evokeThen the first ten minutes, and the SDK's first hour.