Examples · 03 of 06
Run kubectl operations in your team's words.
Natural-language commands for the operations your team approved. A reflex is a small program you install and ask for in your words. Your team writes one per operation, and only those can run. Your service names fill in kubectl's arguments. A scale runs when evoke is sure enough, and a rollback asks every time.
The request and the result
01A scale runs. A rollback waits for a yes.
Four sentences typed in the ops project, the directory that holds these reflexes. Jev, TypeSafe AI's model, read each one and gave every answer a probability. A change runs without a question only when every answer is at least 0.9, the default. Recorded on 21 September 2026.
- 1kubectl, and no code. The
scalereflex is only a manifest, thereflex.tomlfile that describes it. It names kubectl and its arguments, and the sentence fills them in. - 2Your names become kubectl's values. «checkout» is a word in your list, and
its value is
checkout-api. The classifier reads the word and its meaning. kubectl gets the value. - 3A rollback cannot be undone, so it asks. «weakest: service 0.99» names its least sure answer, the service. evoke was that sure, and it still asked. Every value is one your lists allow, but a reading can still be wrong.
The operations
02The scale reflex as a manifest, as code, and in an app.
The scale manifest around kubectl, your service names in a file, and an app of nine
lines that runs one sentence. In code, the manifest becomes an object, and with() passes
your names to it.
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}", ] [needs] writes = ["~/.kube"] # the kubeconfig, and kubectl's cache beside it hosts = ["*"] [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: evoke.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()
// 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()
- 1A reflex that runs a program directly needs no Node. evoke starts kubectl
itself with its arguments, never through a shell. Each placeholder, such as
{service}, fills one whole argument. A value that starts with a dash is refused. - 2
silenceandrollbackhave the same shape.silencereads a duration from the sentence and a host from your words.rollbackis marked destructive. - 3The app asks through two handlers. One asks for a yes or a no. The other
offers a numbered menu for a missing value. Both live in
terminal.ts, which the ops and runbook apps share.
One boundary
03A missing environment is asked for, from your own list.
The sentence named the service and the number, but not where. evoke offered the environments you listed, then checked the call again with your answer.
$ evoke "take payments down to 1 replica" Which environment? [1] staging [2] prod [+] add one > 2 scale service="payments" env="prod" replicas="1" 0.95 deployment.apps/payments-svc scaled
The choices are the words in vocab/envs.toml. With no terminal attached, as in a
cron job, evoke cannot ask. It prints the command to run yourself and exits with code 3.
A file of requests: the inbox example →
Set it up
04Build the scale reflex from this page, then type one sentence.
scale is shown whole above, and it runs kubectl itself, so the terminal route needs no
Node. kubectl changes your cluster for real, so point your words at a deployment you may scale.
- kubectl, with accessOn the machine where you type. A reflex runs as you, held to what its manifest declares: your kubeconfig, and the network.
- Your kubeconfigkubectl reads
~/.kube/config, and its current context picks the cluster. The program gets onlyPATH,HOME,TMPDIR,LANGandTERM, so an exportedKUBECONFIGdoes not reach it. - evoke and a keyInstall evoke and set your key, then come back here.
- Questions about the clusterThis example only changes things. A question like «which pods
restarted?» needs a reflex that reads, around
kubectl get.
-
Make the project
A folder with an
evoke.tomlfile is a project, andadapter = "jev"has Jev read its sentences. Then save the manifest from the files tab above as~/ops/scale/reflex.toml.mkdir -p ~/ops/scale && cd ~/ops && echo 'adapter = "jev"' > evoke.toml -
Trust it, and add the reflex
evoke decides in a new project only after you trust it once. The add checks the manifest and prints one row:
scaleruns kubectl. It stays inactive until its two lists have words.evoke trustevoke add ./scale -
Put in your words
The word is what you type, and the meaning is what the classifier reads. The value is what kubectl gets: here the deployment
checkout-api. A word with no value, likestaging, passes the word itself as the namespace. Put your own deployments and namespaces in place of these.evoke vocab services add checkout "The checkout API." --value checkout-apievoke vocab envs add staging "The pre-production cluster." -
Look at a decision before it runs
evoke trydecides and runs nothing. It shows the ranking, the answer for each input, and what would happen.evoke try "scale checkout to 6 in staging" -
Type the sentence
evoke prints the call it chose and how sure it was, then kubectl's own line. kubectl changes the cluster for real.
evoke "scale checkout to 6 in staging"
To add an operation, copy scale/ to a directory of its own, then change its
description, its examples and its run line. evoke add ./<name> checks the manifest, and warns
when the new reflex would win a phrase that belongs to another one.
In your app
05The same reflex, from a Node application.
app.ts loads ~/ops: its evoke.toml, the scale
reflex and your words. It runs the manifest, so kubectl gets the same five variables as at your terminal.
It needs Node 24.5 or newer, and ES modules.
npm init -y && npm pkg set type=modulenpm install @evoke-build/evokenode app.ts "scale checkout to 3"- 1Save
app.tsin~/ops, andterminal.tsin the folder above it.app.tsimports its two handlers from there. - 2The sentence names no environment, so the app's menu offers the ones you listed. kubectl then scales for real.