Examples · 03 of 06
Operate services in your team's words.
Scale a service, mute an alert, roll back a release. Your service names map to the
arguments kubectl needs. A write runs when the bar is met, and a rollback asks every time. A
pattern for your app: the tooling, the credentials and the cluster access stay yours.
The request and the result
01Scale, silence, roll back.
Three reflexes for one channel. A write runs when the bar is met. A rollback always asks. Every line is what the terminal printed.
- 1kubectl, no code. The
scalereflex is a manifest and nothing else: a program with its arguments, filled from the sentence. - 2Your names, from a vocabulary. «checkout» is a word whose value is
checkout-api. The program gets the value. The classifier sees the word. - 3The rollback is destructive. Sure at 0.99, and it still asked. A release moved only after the yes.
The operations
02The same reflexes as files, as code, and in the app.
A manifest around kubectl, the team's names in a file, and the eight-line app that runs a
sentence. In code, the same text, and with hands the names over.
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()
// 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()
- 1An argv body needs no runtime. Each placeholder is a whole element, filled with the word's value or the number's text. Never through a shell.
- 2
silenceandrollbackfollow the same shape. A duration read from the sentence, a host from your words, and a destructive effect on the rollback. - 3The app is two handlers. A confirm is a question. An ask is a numbered menu. Twenty lines, shared by every example here.
One boundary
03A missing environment is asked for, from your own list.
The sentence named the service and the number, not where. evoke offered the environments you defined, and decided again with the 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 choice came from vocab/envs.toml, never from the classifier. Piped from a file
with no terminal, this line would exit 3 instead, and name the command to run yourself.
Before you run it
04What this pattern needs.
- kubectl, with accessOn the machine that runs the sentence, with the cluster credentials it already uses. evoke adds none.
- Your vocabularyServices and environments, with their real names as values. From a file, or
from your database through
with. - A classifier keyFor every live sentence. The provider bills the use.
- Node, only for codeThe manifest-only
scaleneeds no runtime. The reflexes written in TypeScript need Node 24.5 and the SDK.
The files above are the whole example. Copy them from this page.