Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The evoke manual

Software, by reflex. evoke turns a sentence into a call of a small program. That program is a reflex: a recipe anyone can write, share and improve. A classifier chooses it. A confidence gate decides whether it runs. A sentence can choose a program. It can never invent a value. The classifier is Jev, TypeSafe AI's System One model, reached through either of two built-in adapters. The design is bound to none.

$ evoke "kill the lights in the den"
  lights room="den" state="off"  0.92
den lights off

One tool does three jobs. A CLI where you type what you want. A package manager that installs reflexes from git. A TypeScript SDK that puts the same decisions inside your app. You invoke a function; you evoke a reflex.

Where to start

You want to…Read
Type what you want and see it happenInstall, then The first ten minutes
Learn the words this manual usesConcepts
Write a reflex of your ownYour first reflex
Decide and run reflexes inside your applicationThe SDK
Look something upReference

The manual

Start

PageRead it when
InstallPutting evoke and its SDK on a machine
The first ten minutesYour first key, your first reflexes, your first lesson
ConceptsThe words, and the three steps of a decision

Use: the CLI

PageRead it when
Typing a requestBare input, the REPL, a pipe, --json, --tag, exit codes
LanguagesEnglish, German, French and Spanish: how evoke tells, what stays as typed, what why says
OutcomesRun, confirm, ask and abstain; try, why, run
WeavingSeveral things in one sentence: the plan, what a step takes from another
Installing reflexesadd, remove, update, sync, trust; refs, the lock, the store
Tuningteach, overlays, vocabularies, config, show, test
Calibratingcalibrate: what the confidence meant on your records, and over repeats
ProjectsYour home project, an app's project, evoke.toml, trust

Author: writing reflexes

PageRead it when
Your first reflexnew, check, a try, a run: a reflex in ten minutes
The manifestreflex.toml, key by key
ArgumentsOptions, vocabularies, picks and flags; ranges; renames
The bodyFile bodies and argv bodies; context, results, the deadline
PlaybooksA reflex whose body is a plan: steps with slots, one yes over the whole
Examples and testsRecords, assertions, evoke test, lint
The rulesHow a reflex, a playbook and a set are written, and the checklists
PublishingTags, versions, the contract diff, collections, the Hub

SDK: @evoke-build/evoke

PageRead it when
Getting startedThe first hour: one file, one reflex, one decision
Projects in codeload(), roots, reflexes as code, with(), generated types
DecisionsThe Decision union, fill, run, handle; steps and weave
Adaptersjev(), openjev() and clef(), thresholds, and writing an adapter of your own
Testingreplay(): offline, deterministic tests over a recording
ErrorsThe three errors, and what each asks of you

Reference

PageHolds
CommandsEvery command, flag and exit code
Filesevoke.toml, evoke.lock, overlays, vocabularies, the types
The JSON lineWhat --json prints, field by field
EnvironmentVariables evoke reads and the paths it writes
DiagnosticsEvery line that ends in a fix, and what the fix does
GlossaryThe words, in one place

Around the tool

PageHolds
The collectionThe first-party reflexes: what a Mac does at a word
SecurityWhat runs as you, what is guaranteed, and what is not

Conventions

  • A line beginning $ is a command you type. The lines under it are what the terminal shows.
  • What you asked to see goes to stdout: a reflex's result, or the answer of show, try, why and test. What evoke says about a run goes to stderr, indented two spaces.
  • Every line that needs something from you ends with the command that does it:  → evoke trust.
  • macOS and Linux; Windows through WSL. The collection's programs are for macOS.

evoke is Apache-2.0; the collection is MIT. The name and the logo are trademarks: except for displaying the licence details and identifying us as the origin of the software, you have no right under the licence to use them.

Install

evoke is one small native binary. Reflexes that run a JavaScript file need Node next to it. Nothing else is installed for you.

What you need

ForNeeded
The binarymacOS or Linux; Windows through WSL
Installing reflexes (add, update, sync)git on your PATH; your own git configuration and credentials apply
Reflexes that run a .mts or .mjs fileNode 24 or newer on your PATH when you run evoke add or evoke sync
DecidingA key for the classifier: TYPESAFE_API_KEY for the jev adapter, OPENJEV_API_KEY for openjev

Some reflexes run a program with arguments instead of a JavaScript file. Those are argv reflexes. They need no runtime at all.

The binary

On macOS or Linux, use the script. It fetches the release built for your machine, checks it against the release's SHA256SUMS, and puts evoke in ~/.local/bin. Set EVOKE_INSTALL to choose another directory. Set EVOKE_VERSION to pick a version.

$ curl -fsSL https://evoke.build/install.sh | sh

With mise:

$ mise use --global github:evoke-build/evoke

From source, with a current Rust toolchain:

$ cargo install --locked --git https://github.com/evoke-build/evoke evoke

Every way ends the same:

$ evoke --version
evoke 0.21.0

The archives are on GitHub releases. Each is named evoke-<target>.tar.gz, for aarch64-apple-darwin, x86_64-apple-darwin, aarch64-unknown-linux-musl or x86_64-unknown-linux-musl. The Linux binaries are static. Each archive holds evoke, LICENSE and NOTICE. SHA256SUMS sits beside them, and so does a build attestation, which gh attestation verify <archive> --repo evoke-build/evoke checks. To update, run the script again, or mise upgrade.

The SDK

$ npm install @evoke-build/evoke

The package needs Node 24.5 or newer, and it is ES modules only. It carries the core as WebAssembly, so it has no build step and no dependencies. The SDK has its own manual section: Getting started.

The key

Jev is TypeSafe AI's classifier. evoke reaches it through one of two adapters. Each reads its key from your shell's environment, only at the moment it decides, and never writes it to a file.

The jev adapter is the default. It posts to TypeSafe AI's own API, under a key from typesafe.ai:

$ export TYPESAFE_API_KEY=<value>

The openjev adapter posts to OpenJEV, an independent service that forwards the request to Jev and returns its answers. OpenJEV is not TypeSafe AI's. evoke is affiliated with neither, and vouches for neither. A key from OpenJEV is on OpenJEV's terms, and whether it may offer Jev is a matter between OpenJEV and TypeSafe AI. As of this release it publishes no terms and no privacy policy, so weigh that before you choose it. Name it in evoke.toml and export its key:

adapter = "openjev"
$ export OPENJEV_API_KEY=<value>

Without the key, the first decision prints a line naming the one it needs, and exits 3. Both adapters ask the same questions and ship the same bars, so a project moves from one to the other with that one line.

Next: The first ten minutes.

The first ten minutes

Ten minutes, from an empty machine to a reflex that learned a phrase of yours. Every output line below is what the terminal shows.

1. Say something

$ evoke "lock the screen"
  jev needs TYPESAFE_API_KEY, a key from typesafe.ai  →  export TYPESAFE_API_KEY=<value>
[3]

Two things happened. evoke wrote your home project: the file ~/.config/evoke/evoke.toml, with one line, adapter = "jev". And it told you the one thing it needs. [3] is the exit code. It means needs a human. Export the key and try again:

$ evoke "lock the screen"
  no reflexes are installed  →  evoke add evoke-build/reflexes
[3]

2. Install the collection

evoke-build/reflexes is the first-party collection: fifteen reflexes for what a Mac does at a word. Installing a repository installs every reflex in it.

$ evoke add evoke-build/reflexes
+ awake       evoke-build/reflexes/awake 0.1.0       write        runs awake.mts
              needs runs caffeinate
+ download    evoke-build/reflexes/download 0.1.0    write        runs download.mts
              needs writes {to} ~/Downloads · hosts *
+ lock        evoke-build/reflexes/lock 0.1.0        write        runs lock.mts
              needs runs open
+ mail        evoke-build/reflexes/mail 0.1.0        write        runs mail.mts
              needs runs open
+ note        evoke-build/reflexes/note 0.1.0        write        runs note.mts
              needs writes {file}
+ open        evoke-build/reflexes/open 0.1.0        read         runs open
              needs reads {place}
+ power       evoke-build/reflexes/power 0.1.0       destructive  runs power.mts
              needs runs osascript
+ screenshot  evoke-build/reflexes/screenshot 0.1.0  write        runs screenshot.mts
              needs writes ~/Desktop · runs screencapture
+ sleep       evoke-build/reflexes/sleep 0.1.0       write        runs sleep.mts
              needs runs pmset
+ sound       evoke-build/reflexes/sound 0.1.0       write        runs sound.mts
              needs runs osascript
+ timer       evoke-build/reflexes/timer 0.1.0       write        runs timer.mts
              needs runs osascript
+ trash       evoke-build/reflexes/trash 0.1.0       destructive  runs osascript
+ visit       evoke-build/reflexes/visit 0.1.0       read         runs visit.mts
              needs reads ~/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist · runs open plutil
+ volume      evoke-build/reflexes/volume 0.1.0      write        runs volume.mts
              needs runs osascript
+ wifi        evoke-build/reflexes/wifi 0.1.0        write        runs wifi.mts
              needs runs networksetup
  inactive  note: config "file" is not set      →  evoke config note file <value>
  inactive  open: vocabulary "places" is empty  →  evoke vocab places add <word> "<meaning>"
  inactive  visit: vocabulary "sites" is empty  →  evoke vocab sites add <word> "<meaning>"

One row per reflex: its local name, where it came from, its tag, its effect, and what it runs. Under it, what it touches: the paths, the network as hosts *, the programs; a row without that line declares nothing, and its body touches nothing but its own folder. The kernel holds each body to that line: Security. Then the inactive lines. A reflex that needs your words or a setting stays out of every decision until it has them. Each line ends with the command that gives it what it lacks. Nothing is guessed for you.

3. Use it

$ evoke "set the volume to 40 percent"
  volume level="40 percent"  0.96
volume 40%

The indented line is evoke's own, on stderr. It shows the call it chose and its confidence. The last line is the reflex's result, on stdout. The confidence was over the bar for a write reflex, so it ran without asking.

A less certain decision confirms first. [t]each records what you meant, then runs:

$ evoke "kill the wifi"
  wifi state="off" · write · weakest: state 0.86
    a write runs at 0.90 or more
  Turn Wi-Fi off?  [y]es [n]o [t]each > t
+ overlays/wifi.toml  [examples] "kill the wifi" = { state = "off" }
wi-fi off

That one line in overlays/wifi.toml is yours. It is an example the classifier now sees, in a file no update will touch.

A destructive one always confirms, however sure:

$ evoke "restart the computer"
  power action="restart" · destructive · weakest: route 0.98
    it cannot be undone, so it always waits for a yes
  Make the laptop restart now?  [y]es [n]o [t]each > n
[2]

[2] means declined: you said no, or nothing fit. When what you said fits nothing, evoke abstains and shows you the ranking, and reminds you of what is not yet in play:

$ evoke "what time is it"
  none of them 0.85 · timer 0.10 · awake 0.03 · lock 0.02
  note, open and visit are inactive  →  evoke show
[2]

4. Give it your words

Some reflexes need words only you can supply: your folders, your sites. A vocabulary is that list. It is one file you own, and every reflex that names it reads it.

$ evoke vocab places add desktop "The desktop." --value "~/Desktop"
+ vocab/places.toml  desktop = { what = "The desktop.", value = "~/Desktop" }
$ evoke "open my desktop folder"
  open place="desktop"  0.95

The meaning is what the classifier reads. The value is what the reflex receives: open opened that folder. A setting works the same way:

$ evoke config note file "~/notes.txt"
+ evoke.toml  [config.note] file = "~/notes.txt"

The notes file must exist, since the reflex declares it: touch ~/notes.txt first. A secret is only ever named, never stored: evoke config <reflex> <key> --env <VAR> writes the variable's name, and the value is read from your environment when the reflex runs.

5. Teach it

Teaching works from the command line too, and you can teach what something is not:

$ evoke teach "go dark" wifi state=off
+ overlays/wifi.toml  [examples] "go dark" = { state = "off" }
$ evoke teach "what time is it" not timer
+ overlays/timer.toml  [examples] "what time is it" = false

6. Look inside

try decides without running and shows every judgment. why explains the last sentence. show prints what is installed, or one reflex as it is used, with + next to every line that is yours.

$ evoke try "kill the wifi"
  "kill the wifi"

  which reflex            wifi 0.95   (none of them 0.03 · power 0.01)
                          Turn Wi-Fi on or off.
  state                   off 0.86   (on 0.10 · not said 0.04)
                          asked a second way: off 0.86; the less sure of the two counts
                          is it off? yes 0.95
  → waits for a yes: wifi state="off" · write · weakest: state 0.86
    a write runs at 0.90 or more

  replay answered 25 questions in 2 rounds

Which reflex, then each value with what it stands on, then what would happen and why. The weakest answer is the confidence. It sits under the write bar, so this one would wait for a yes.

Where things are

WhatWhere
Your project: evoke.toml, evoke.lock, overlays/, vocab/~/.config/evoke/
Installed code, keyed by content hash~/.cache/evoke/store/
The decision log, the REPL's history~/.local/state/evoke/

Your project holds only what you wrote, the lock, and generated types. Put it in your dotfiles. Everything else is a cache, and evoke sync rebuilds it on a new machine.

Next: Concepts, or straight to Typing a request.

Concepts

Seven words, three steps, and one idea that makes the rest follow.

The idea

The classifier evoke uses answers closed questions: which of these? is this stated? yes or no? It answers with calibrated probabilities. Everything it knows about a reflex comes from the reflex's own description, sent in the request. So the wording is the tuning, and one line is documentation, tuning, test and customization at once:

"kill the lights" = { state = "off" }

A reflex is a package of such wording plus the program it describes. evoke fetches, versions and refines that wording, and runs the program. The rules say how to write it so that it is read right.

The words

ConceptMeaning
ReflexA directory: reflex.toml and the file or program it names. Its identity is its location, owner/repo[/dir] at a git tag.
Manifestreflex.toml: what the classifier reads and what the program receives. Its wording is yours to override. Its contract is not: argument names, option keys, sources, ranges, what it runs.
ProjectA directory of files you own: evoke.toml, evoke.lock, overlays/, vocab/. Your home project lives in ~/.config/evoke/. An application has its own at its root.
OverlayYour wording for one reflex, in one file, laid over the shipped manifest. It adds and replaces. It never deletes.
VocabularyYour word = "meaning" list, shared by every argument that names it. A package never ships or writes one.
AdapterThe classifier behind a decision, by name. jev and openjev both reach Jev, through TypeSafe AI's API or through OpenJEV. The core names no engine.
RuntimeWhat runs a program: Node for a .mts or .mjs file, nothing for an argv. Chosen from the manifest. Authors never see it.

Two more words come up everywhere. A call is a reflex with its arguments filled in, on one line: lights room="den" state="off". An effect is what running a reflex does to the world: read, write or destructive. The effect decides how sure evoke must be before it runs without asking.

A tag is a word a manifest carries, so --tag can narrow a decision. A collection is a repository of reflex directories. The Hub is where reflexes are found. For now, that is the first-party collection, evoke-build/reflexes.

The three steps

evoke "mute the office lights"
  1. Decide. Every active reflex's manifest becomes questions. Which reflex does the input ask for? For each argument, which value, or was it left unstated, or stated in words that are not among the choices? One request carries all the questions. The adapter answers each with a probability. Where an answer leaves a doubt, the reading checks itself with another question: it asks about a value a second way, whether a word of the input means the value it seems to, what the words no value holds do, and, before a call runs, whether the call holds all the input says. What it reads, it reads from your words; nothing is invented.
  2. Gate. Confidence is the weakest of those answers. Each effect has a bar. Against that bar, the outcome is settled: run, confirm, ask for a missing argument, or abstain. A destructive reflex always confirms. Code stays in control: the classifier proposes, the gate disposes.
  3. Run. The reflex's program runs with its arguments, a clean environment and a deadline. Its one line of text comes back.

What the classifier can and cannot do

It selects. An argument's value is one of five things: one of the author's options, one of your vocabulary words, a piece of what you typed, taken word for word, or, in a sentence of several steps, a field of an earlier step's result or that step's whole result, which the plan shows; or, when the words leave it out, a value an earlier result of this session holds, which you choose at the prompt. That piece can be a number, a duration, an email address, a URL, a quoted phrase, a date, a time, an amount or a code, and it is checked against its range. So a sentence can choose a call, but it can never invent a value. And what it chooses still passes the gate. That is the shape of every guarantee in Security.

Next: Typing a request.

Typing a request

evoke takes one sentence and does three things with it: decide, gate, run. This page is about how the sentence gets in and how the answer comes out.

Three ways in

evoke "kill the lights in the den"     one input, as the argument
evoke                                  the REPL, on a terminal
echo "kill the lights" | evoke         a filter: one input per line of stdin

As the argument. Everything after evoke is the input, unless the first word is exactly a command name. Bare words are joined by one space, so evoke kill the lights and evoke "kill the lights" are the same call. Quote a sentence that holds a character the shell would take, such as ? or *.

The REPL. Run evoke alone on a terminal. It prompts > and decides each line as if you had typed it as the argument. It skips blank lines. It keeps one warm connection to the adapter. You can edit the line, and the arrow keys recall earlier ones from ~/.local/state/evoke/history. Ctrl-C cancels the line. Ctrl-D on an empty line ends the session with exit 0. Under TERM=dumb the line is read plain: no editing, no history, and Ctrl-C ends the session.

$ evoke
> kill the lights in the den
  lights room="den" state="off"  0.92
den lights off
> what time is it
  none of them 0.85 · timer 0.10 · lights 0.03 · volume 0.02
>

The session remembers what its reflexes returned. A pick whose author named a recent field offers those values back when a sentence leaves the argument out, so after a listing, «roll back the last deploy» asks with the releases numbered; words that point at one, «buy it» after a product shown, «pay my last order», take the newest without asking (Arguments):

> list the checkout deploys
checkout: 2 deploys today, the last 4.12.0 at 13:58
> roll back the last deploy
  From which release?  [1] 4.12.0  [2] 4.11.3  > 1
  Roll back 4.12.0?  [y]es [n]o [t]each > y

A day in a sentence, tomorrow, next friday, the 14th, is read as you said it and becomes a date when the program runs, against your machine's clock (Arguments).

Words the reading takes as they are

A value is always one of your own words. A text an argument wants between quotes, like a timer's label, is read from the words you typed without them: in set a timer for 10 minutes called tea the label is tea, and the call waits for your yes, since the words could have been cut otherwise. A word that stands for a value from a list but is not on it — the snug for a room called den, garage for no room at all — is read where the list's meanings say what it is, and the call waits for a yes; where they do not, evoke asks, and names the word: "garage" is not on the list. evoke why shows each of these under the value it gave (Outcomes).

Languages

evoke reads a request typed in English, German, French or Spanish, and there is nothing to set: the words of the request say which language it is in, and a request that mixes two is read in both. A value stays as you typed it and is never translated: zieh die Umsätze für August reads the month, timer fünfundvierzig Minuten für die Wäsche reads the length and keeps Wäsche as the label, treinta y uno is 31. The questions evoke asks you and what it says back stay English in this release. evoke why names the language that read a request when it was not English, with the words that showed it (Languages).

A value typed the way you say it

You can type a value the way you would say it aloud. evoke reads an address, a URL, a code, a number, a day or a length of time this way: dana dot weiss at example dot org, k d oh three one five, seven oh, an hour and twenty minutes. The call shows the value as it would be typed.

A code takes the shape of the reflex's own examples. Where every example reads like HS-0409, your letters become capitals and the dash is added. The call then waits for your yes, with your words on the line:

$ evoke "earmark k d oh three one five"
  reserve sku="KD-0315" · write · weakest: route 0.95
    sku was read from "k d oh three one five"
  Reserve every unit of KD-0315?  [y]es [n]o [t]each > n

Some words can be read in more than one way. ops twenty at example dot com may be ops20@example.com or 20@example.com, and thirty one fifteen may be the start of a longer code. evoke does not choose for you. It asks, and offers what your words read as (Outcomes):

$ evoke "email the update to ops twenty at example dot com"
  To which email address?  you wrote "ops twenty at example dot com"  [1] ops20@example.com  [2] 20@example.com  [0] none of these  > 1
  email to="ops20@example.com" · destructive · weakest: route 0.95
    it cannot be undone, so it always waits for a yes
  Email the shipment update to ops20@example.com?  [y]es [n]o [t]each > n

A name typed another way

A word of your vocabulary may be typed in a form that is not on the list. evoke reads it only where your words leave no doubt about which listed word they name.

  • A word cut short or typed with a slip is read where your own words name the listed word: sept for september, eu-wwest for eu-west. evoke why shows the check under the value, as asked of "sept" alone.
  • A longer name, a pet name or a name one letter away may be another person: Samuel, Sammy or Sal beside a colleague listed as sam. evoke never reads such a name as the listed word. It asks, and puts the nearest word ready. A y takes it:
$ evoke "where is Samuel's laptop"
  Which colleague's laptop?  you wrote "Samuel's": sam?  [y]es, or  [1] sam  [2] ana  [3] jo  [+] add one  [0] none of these  > y
  device person="sam"  0.95
sam: serial C02XK1ABJG5M, last seen 07:41

A filter. Pipe lines into evoke, or into evoke try, and every line is one input, answered in order. A line that needs a prompt, a confirm or an ask, cannot be answered without a terminal. That line exits 3 and names the command to run yourself. The filter still answers every other line, and exits with the first non-zero code.

$ printf 'kill the lights in the den\nstart a timer\n' | evoke
  lights room="den" state="off"  0.92
den lights off
  an ask needs a terminal  →  evoke "start a timer"
[3]

What comes out

What you asked to see goes to stdout: a reflex's result, and the answer of show, vocab, why, try, test and check. What evoke says about a run goes to stderr, indented two spaces: the call and its confidence, a prompt, the line of a write, the ranking after an abstain, and every diagnostic. So evoke "…" > out.txt captures the result alone, and evoke show | grep timer finds the row. A script reads the rest from the exit code.

$ evoke "kill the lights in the den"
  lights room="den" state="off"  0.92          ← stderr: the call, and the confidence it ran on
den lights off                                 ← stdout: what the reflex returned

Flags

FlagDoes
--jsonOne JSON line per input instead of the lines above. It holds the input, the decision, the trace and the result. This is the filter format: see The JSON line.
--tag <tag>Offers only the reflexes carrying the tag. Repeat it to widen: --tag home --tag sound.
--The rest is input, even when it begins with a command word: evoke -- test the alarm.

--help, -h or help, and --version or -V, only count in the first place. They print to stdout. evoke add --help prints the help too, as does -h right after any command word. To say the word help as a sentence, put -- before it.

A command word written after a flag, like evoke --json try "lock it", is refused rather than decided. The line names the command and how to write it. Nothing runs by accident.

Command words

The first argument selects a command only when it is exactly one of these words:

help  try  why  run  add  remove  update  sync  trust  show  teach  vocab  config  test  calibrate  new  check

Four more words are reserved for later: edit, search, publish, adapter. Reserving them now means adding one later never changes what a sentence means. Lines from stdin are always input, whatever they begin with.

Exit codes

CodeMeaning
0Ran. Also --help, --version, and every command that did what it said
1The program, or the machine, failed. Also evoke test with a failing case, and evoke calibrate with a call wrong at or over its bar
2Abstained, or you declined
3Needs a human: a missing key, an untrusted project, a prompt with no terminal, a line to fix
4The adapter failed

Ctrl-C while a body runs ends the body and everything it started, a JavaScript body's signal aborting first; then evoke ends as an interrupted program does, and the shell reports exit 130. In a weave, every step that did not finish reads skipped · cancelled (Weaving). Anywhere else, Ctrl-C ends evoke at once.

Limits and time

An input over 2 000 characters is refused. One decision has 30 seconds. The classifier's answer and the program's run share them. A prompt waiting for you never counts. When the deadline passes, evoke stops and says so. It never guesses.

Colour

On a terminal, evoke colours what matters. The reflex's name in a call. The top answer of every judgment try shows. The effect: green for read, yellow for write, red for destructive. The weakest judgment, dimmed. The → of a fix. The + and - of a write. A spinner turns while a request or a fetch is in flight, and counts the cases of a test. Piped, logged, under NO_COLOR, or with TERM=dumb, the same words print plain. On a terminal narrower than the help, --help puts each description under its command.

Next: Outcomes.

Languages

evoke reads a request in English, German, French or Spanish. Each language is a pack of words built into evoke: the words that join two steps, the ones that ask for nothing, the pronouns that point back, the numbers, the days and months, the clock, the units and the currencies, the signs said aloud. There is nothing to install and nothing to set.

How it tells

The words of the request show the language. A request holding more of one pack's own words than of any other's is read by that pack, and by English always; one that mixes two languages, Zeig mir die Sessions von Ana, por favor, is read by both where they show alike. A request so short that no pack shows, wipe C02TM6PNHV29, is read by every pack, each holding back its two-letter words and the words English knows. A code, an address or a link shows no language.

What stays as typed

A value is always one of your own words, in your own language: a day, a number, a length of time, an amount, a time of day said in German is read as the value the program takes, and a text — a label, a note, a message — is kept as typed, never translated. The questions evoke asks you, its prompts and why's lines stay English in this release.

At a confirm prompt a yes or a no in any of the four languages does: y, yes, ja, oui, sí run; n, no, nein, non decline (Outcomes).

What why says

evoke why and evoke try name the language that read the request when it was not English, with the words that showed it:

  read by                 German, shown by «für», «den»

An English request says nothing of the kind. evoke calibrate and evoke test print a line per language other than English that read a record, with how its records fared (Calibrating, Tuning).

What a change to a pack does

A plan file carries the digest of every pack it was read with. A release that changes a pack refuses a plan file made under the old one as stale, as it does when a question's wording changes. evoke run then names the evoke try --save that makes the plan again (Weaving).

Outcomes

Every input ends in one of four outcomes. This page shows each one and what decides it. It also covers the three commands that look at a decision without changing anything: try, why and run.

Run

Confidence cleared the bar for the winner's effect. Every required argument was stated. Nothing else needed attention. The call prints with its confidence, the program runs, and the result prints.

$ evoke "kill the lights in the den"
  lights room="den" state="off"  0.92
den lights off

Confirm

The call is complete, but something holds it at a question. evoke's own line names the call, the effect and the weakest judgment; the line under it says why the call waits. Then comes the reflex's one-line prompt, and three answers.

$ evoke "dim the office"
  lights room="office" state="dim" · write · weakest: state 0.85
    a write runs at 0.90 or more
  Set the office lights dim?  [y]es [n]o [t]each > y
group-7 lights dim
AnswerDoes
y, yesRuns; so does a yes in any language evoke reads, ja, oui, sí (Languages)
n, noDeclines, exit 2; so does nein, non
t, teachRecords what you said as an example of this call in your overlay, then runs

These are the reasons a decision stops at confirm, each with the line it prints; several print on one line, separated by ;.

  • destructive: a destructive reflex always confirms, however sure. it cannot be undone, so it always waits for a yes.
  • no gate: the adapter shipped no thresholds, so nothing runs on its own. the adapter ships no bars, so nothing runs on its own.
  • under the bar: the weakest judgment is under the bar for this effect. a write runs at 0.90 or more.
  • read from your words: a value from a list was taken from a word of the input, where the answers about it did not agree; or a value was read from words that do not spell it as it is typed, like a day misspelt, a code typed with a space, or a code said aloud that took its capitals or its dash from the reflex's examples. room was read from "snug", sku was read from "hs 0409".
  • a text without quotes: a text was read from words you typed without quotes. label was typed without quotes, which always waits for a yes.
  • more words: the input holds words that ask for another thing, which the call does not hold. "lock the door too" asks for another thing. Words that say the call is wanted once more, like too or the same for, count as these when the plan holds the call once.
  • part of a value: words right after a value answer the same question, so the value may be cut short of them. "30" may be part of the duration.
  • a text in quotes: the input holds a text in quotes that no value took, and the reflex takes one. 'tea' is in quotes and the call does not hold it.
  • a text left over: the reflex takes a text, the call holds none, and the input has words left that say what is wanted from the result. They may be that text. "saying tea is ready" may be the label.
  • less than you typed: the call, read back against the input, leaves out part of what you typed. it holds all you said at 0.20, and a call runs at 0.30 or more; it leaves out "in the morning". The words named are ones no value holds: words a value was read from are not among them. Words that say what you want from the result are named apart: "newest first" says what is wanted from the result.
  • a detail beside it: a part of the sentence that is not in the plan may add a detail to this step; or the step was cut from a longer part read with a value its call lacks; or it was cut from the part beside it at a sign typed for and, and the cut is not sure. "the ones since noon" may add a detail this call does not hold (Weaving).
  • read with the whole sentence: the step's words match nothing alone, and the step comes from reading them with the rest of the sentence. "payments too" matches nothing alone, and the whole request reads it as errors at 0.85, which always waits for a yes (Weaving).
  • one of several: the value was read from words that point at several things named earlier, like both, which make one step for each of them. person was read from "both" (Weaving).

[t]each records only what the input stated. An argument you filled in at a prompt is not recorded. The answer is read from the terminal, never from stdin. With no terminal, a confirm exits 3 with the command to run yourself.

Ask

The winner is clear, but an argument is missing. The input never stated a required one, or the value fell outside its range, or the input names a value that is not among the argument's choices, or it names one that could be read two ways. evoke asks the argument's own question, offers what it may be, and gates again with your answer.

$ evoke "kill the lights"
  Which room?  [1] den  [2] office  [+] add one  [0] none of these  > 1
  lights room="den" state="off" · write · weakest: state 0.74
    a write runs at 0.90 or more
  Set the den lights off?  [y]es [n]o [t]each > y
den lights off
$ evoke "set the volume to 150 percent"
  How loud, in percent?  150 percent is outside 0–100  > 40
  volume level="40"  0.96
volume set to 40%
$ evoke "kill the lights in the garage"
  Which room?  "garage" is not on the list  [1] den  [2] office  [+] add one  [0] none of these  > 2
  lights room="office" state="off"  0.95
group-7 lights off

The prompt names the words of the input that answer the question, where evoke found them.

  • A choice takes its number or its own text. A pick takes what you type, read the same way as the input. A pick is a number, a duration, an address, a URL, a quoted phrase, a date, a time, an amount or a code. A quoted argument takes the whole line when nothing is quoted; a code takes a quoted one.
  • A pick whose manifest names a recent field lists what the bodies of this session returned under it, From which release? [1] 4.12.0 [2] 4.11.3 > , and a number picks one; words that point at one, it, that order, my last order, take the newest with no prompt (Arguments).
  • Where one listed word is the likely answer, the prompt puts it ready before the choices: Which colleague's laptop? you wrote "Samuel's": sam? [y]es, or [1] sam [2] ana [3] jo [+] add one [0] none of these > . y takes it. The choices keep their order, and a number or a word still picks any of them (Saying things).
  • Where your words say a pick's value aloud and can be read in more than one way, the prompt quotes them and offers what they read as: Which product code? you wrote "k d thirty one fifteen" [1] KD-3115 [0] none of these > . A number picks one, and you can still type the value yourself. For a number argument the readings are named without a number, you wrote "three eleven" 311 > , since a number you type is the answer itself (Saying things).
  • An answer that does not fit is asked again, with the reason on the line. An empty line asks again.
  • + at a vocabulary's prompt asks Word? and Meaning?, then Path? when a reflex's declaration takes the word's value as a path, writes the word to your vocabulary, and goes on.
  • 0, none of these, declines a listed question, as the end of input, Ctrl-D, declines any: nothing runs, the line says so, and the exit code is 2.

Abstain

none won the route, or the winner is under the route bar. The ranking prints, nothing runs, and the exit code is 2.

$ evoke "make it cosy"
  lights 0.45 · none of them 0.40 · timer 0.10 · volume 0.05 · a reflex is picked at 0.50 or more
[2]

An inactive reflex is never in the ranking. When one is left out, an abstain names it on the next line, and evoke show says why:

$ evoke "what time is it"
  none of them 0.85 · timer 0.10 · lights 0.03 · volume 0.02
  open is inactive  →  evoke show
[2]

What confidence is

For each decision, the adapter answers a question about the route and questions about each argument. Confidence is the lowest probability among the route and every value the call holds. Each value counts by what gave it: where two questions about it agree, the less sure of the two counts. A flag, and an argument the input says nothing of, add nothing to it. An argument you typed at a prompt is settled, and its judgment leaves the gate.

A call that clears the bar of its effect is then read back against the input, in the reflex's own words: its whole description, then each argument with its value. One more question is asked: does this reading hold everything the input says? The call runs when the answer clears a bar of its own, whole. The words no value took are read beside it, each with what the reflex does: they say what to do, answer an argument, say what is wanted from the result, ask for nothing, or ask for another thing.

The bars come from the adapter. Both built-in adapters reach Jev and ship the same numbers. They are calibrated, so each number means the probability this is right, as evoke calibrate measures it on your own records (Calibrating):

FloorDefaultGates
route0.5Under it, abstain
read0.8A read reflex runs at or above it
write0.9A write reflex runs at or above it
whole0.3A call that would run does so when it holds all you typed at or above it
fits0.3At add, a new reflex that fits another's example at or above it is named
destructive—Always confirms

A write changes something, so it asks for more than a read. whole is a second look at a call that already cleared its bar, so it asks for less: it holds a call that leaves part of the input out, and lets through one the answer only hesitates on.

You may raise or lower them for your own machine, under your adapter's table in evoke.toml, [adapters.jev] or [adapters.openjev]. read may never exceed write. No number makes a destructive reflex skip its question. There is no --yes. To run unattended, set a bar in a file you own, not a flag on a pipeline.

[adapters.jev]
gate = { write = 0.95 }

try: decide, and show the work

evoke try "<input>" decides without running, and prints the sentence as it was read. First the sentence, then, where it matters, how the sentence was read as a whole: whether it asks one thing or several and where it was cut, each part set aside or kept out of the plan, and what was left out because you said not to do it. A part set aside by its own words says what it is: "before I forget": about what you do, not a request, "if you would": a courtesy. Then each step, under its number when there are several, with a row a judgment:

  • which reflex: the answers about the reflex, the most probable first, then what the reflex does.
  • One row per argument the call holds, asks, or the input says something of: the answer, or the value where an answer did not give it, and under it what the value stands on — what a second question about it answered, the words of the input that hold it, the yes that took it, the step it was taken from, the part of the sentence that gave it, or your own answer at the prompt.
  • holds all you said: how far the call holds all you typed, where the call was read back against the input; under the bar, the words it leaves out, and the words that say what is wanted from the result.
  • What a rule did: the same call, a part that adds to this call; read with, how a part that matched nothing alone was settled; read apart, how a step was read on its own, out of a longer part; another value, what the classifier answered about the value a step was written for (Weaving); the playbook, the playbook that wrote the step; takes, the results it takes whole; runs if, what picks it.
  • →: what would become of the call — runs, waits for a yes, asks, refused — with the call as the plan prints it, and under it why.

A last line says how many questions the adapter answered and in how many rounds, or that the cache did. An answer's key prints in words: not said for a question the input says nothing to, said, and not on the list for a value that is not among the choices, none of them for no reflex. try shares the cache with a real decision. It is never logged.

$ evoke try "kill the lights"
  "kill the lights"

  which reflex            lights 0.95   (none of them 0.03 · timer 0.01)
                          Turn the lights in one room on, off, or dim them.
  room                    not said 0.87   (den 0.10 · office 0.03)
  state                   off 0.74   (on 0.19 · dim 0.06)
                          asked a second way: off 0.74; the less sure of the two counts
                          is it off? yes 0.95
  → asks: lights state="off" room=?
    it needs room

  replay answered 11 questions in 2 rounds

try --json prints the decision as one JSON line: The JSON line.

why: the last sentence, explained

Every real decision is logged. evoke why prints the last sentence as try would have, from the log alone, and says what became of each step: ran, ran at your yes, declined, asked, refused, failed, cancelled, skipped. After a sentence of several steps, it shows each step under its number (Weaving).

$ evoke why
  "kill the lights in the den"

  which reflex            lights 0.95   (none of them 0.03 · timer 0.01)
                          Turn the lights in one room on, off, or dim them.
  room                    den 0.92   (not said 0.05 · office 0.03)
                          asked a second way: den 0.92; the less sure of the two counts
  state                   off 0.94   (on 0.03 · dim 0.02)
                          asked a second way: off 0.94; the less sure of the two counts
                          is it off? yes 0.95
  holds all you said      0.90
  → ran: lights room="den" state="off"  0.92
    a write runs at 0.90 or more, and its least sure judgment is room, 0.92; it holds all you said at 0.90, and a call runs at 0.30 or more

  replay answered 13 questions in 3 rounds

The last line counts the questions the adapter answered, and the rounds they were asked in: the parts of a sentence are asked about side by side, and the answers to one round may open the next. When every answer came from the cache, it reads answered from the cache; a plan run from a file names the file.

run: by name, no classifier

evoke run <call> runs a call you write yourself. Nothing is decided, so nothing prints but the call. The effect policy still holds: a destructive call confirms. Every value is checked, just as a decision's would be.

$ evoke run lights room=den state=off
  lights room="den" state="off"
den lights off
$ evoke run power action=restart
  power action="restart" · destructive
    it cannot be undone, so it always waits for a yes
  Make the laptop restart now?  [y]es [n]o > y
restart in 5 seconds
$ evoke run lights state=off
  lights: lights needs room  →  evoke show lights
[3]

The call grammar is name arg=value…. A value with spaces is quoted: duration="10 minutes". A flag is its bare name. A word the vocabulary lacks, an option not offered, a number out of range, or an argument the reflex does not have: each is refused, with the command that shows what is allowed. A call by name is not logged. evoke run --json <call> prints the call and its result as one line, for a script: The JSON line.

The cache

The adapter's answers are cached by the installed set, the sentence and the questions asked, not by the decision. Repeat an input and it costs nothing. A decision narrowed with --tag asks fewer questions, so it has an entry of its own. Change a threshold, and the same answers gate differently. try and why explain a cached decision exactly like a fresh one. evoke test never uses the cache.

Next: Weaving.

Weaving

evoke reads every sentence for its steps. One step is decided as it always was, unless its reflex takes an earlier step's result: then the request stops before anything runs. More than one is a weave: each part decided on its own, in the order the words give, a result of one step threaded into a later one, and the plan shown before anything runs.

$ evoke "kill the lights in the den and start a 10 minute timer"
  1  lights room="den" state="off"  0.92
  2  timer duration="10 minute"  0.95
den lights off
10 minute timer started

How a sentence is read

  • A connective can separate two things: and, then, but, a comma, after, before. At each one the classifier is asked whether it does. "kill the lights in the den and start a 10 minute timer" is two things. "set a timer for 10 minutes and 30 seconds" is one. A sentence with more than two dozen such points, a pasted list, is decided as one input.
  • A sign or a letter typed for and is such a point too: +, &, n, nd, adn, alone between two words. "check the errors for checkout n pull its logs" is asked about at n. It is no such point beside a number or a single letter, 2 + 2, R & D, between two capitalised words, Hartwell & Sons, after a comma, or inside quotes. Where the classifier cuts at such a sign without being sure, the two parts may be one request: each confirms before it runs, naming the other, "start a 10 minute timer" may add a detail this call does not hold.
  • With the connectives, the classifier is asked how many things the sentence asks for. Where it says one, the sentence is one step, and no connective cuts it: "check the errors for checkout, and only in eu-west". Two cases still give several steps. One action asked for several values is a step for each value (One action for several values). And a cut at a sign typed for and that the classifier was not sure of stands when each part, read alone, asks for a different reflex. Each part then waits for a yes that names the other.
  • Each part is decided as one input is: routed, gated, its arguments read. A part that matches nothing on its own is tried as another item of its neighbour's task first: "check stock for widgets and gadgets" is two stock checks. So is a part that is only a determiner and one word, "the office" in "kill the lights in the den and the office", whatever it would mean on its own: it stays a step of its own only when it fits no item of its neighbour's task. A part read as an item must be the value it stands in for, "gadgets" for "widgets", never a longer phrase that holds one, and it never stands in for a text in quotes: in start a 10 minute timer called "pasta", and ring me after, no second timer is made. When the classifier was sure the two parts were separate things and one still matches nothing, the whole request is refused rather than half done. When it was not sure, the part is read with its neighbour as one request, and evoke why says what its words do.
  • A part that holds only values, with no word of its own for what to do, continues the step before it: in buy 3 of OUT-503 and 2 of BOK-603, the second part is a purchase too. Read alone, such a part is a toss between the reflexes that take such values, here buying and filling the cart. So it is never left to that reading: the part is put in the words of the step before it, buy 2 of BOK-603, and decided as that step's reflex, whatever it looked like alone, so that the same sentence reads the same way every time. evoke why shows the words it was read in, and read with the words of the part before it, as one request. A word like more, another or too beside the values changes nothing: and 2 more of BOK-603 is a purchase as well. A part that names its own action is read as it always was: add "aurora kettle" to my cart and check out is two different steps.
  • Two verbs before one thing are two steps: in buy and pay 3 of OUT-503, the product is bought, then the order is paid, the second step taking the order the first one yields, as buy 3 of OUT-503 and pay for it does. evoke why shows the second step as read apart, a second thing to do before the object of the part before it, which it takes from. The same holds where the classifier cuts the sentence at the and, buy and pay 2 of HOM-403, and with two things, buy and pay 3 of OUT-503 and 2 of BOK-603: each purchase is paid. A second verb that names the same reflex, or none, changes nothing; and pay order 1024 after buy and keeps its own order, since the words name it.
  • A part that begins with not, don't, never or without is left out, however the apostrophe is typed. What you ask evoke not to do is no step. One step beside such a part is decided as one input. A sentence that is only such parts is nothing to do, and one line says so. A part that also names a value the step beside it lacks is read with that step: in "pull the payments logs, not in us-east, in eu-west" the logs are pulled in eu-west. Where what follows the part is a request of its own, it is kept as one: in "not the office, kill the lights in the den", the office is left out and the den's lights go off. And where the classifier says the sentence asks for one thing, nothing is left out: the sentence is read whole, what it rules out with what it asks.
  • then, after that and next order the steps. after you X, Y and Y after you X read as X, then Y. before you X, Y and Y before you X read as Y, then X. When a write is among the steps, every step runs alone, in the order you wrote them. A plan of reads may run them side by side.
  • A word the sentence says once for several steps reaches each of them. "check checkout's errors, deploys and logs in eu-west" names the service and the region once: every lookup takes both. A step that lacks a required word takes the one word of its kind the sentence names, as an answer would give it. A step that could take an optional word takes the one said once in its part of the sentence, before any then, written into its words and decided again under the same gate. A step whose own words name a word of that kind keeps its own, and a sentence that names two, "checkout" and "payments", carries neither. Only a word from a vocabulary is carried: never a number, a date, an address, a quoted value or an option. And only a word no one reflex owns: a vocabulary one reflex alone asks for, the folder open opens, is that reflex's own and never reaches another step. The plan shows the word on each step it reached, and evoke why says which words were shared.
  • A value one part states can be another part's where the classifier says so. It is asked only for a part whose words point at something, "it", "there", "the same", or for a word from a vocabulary that several reflexes ask for: in "check the errors for checkout in eu-west, then pull its logs there", the logs are pulled in eu-west. An option or a flag of a call reaches a part that asks for the same again, "the same", "likewise", "as before": in "take a screenshot of the window to the clipboard, then the same for the whole screen", both go to the clipboard. evoke why says where the value came from.
  • Two parts that read as the same call, no value of one differing from the other's, are one step: "check the errors for checkout, and the errors in eu-west" is one check, of checkout in eu-west. Where a part could repeat either of two calls, its own words pick one, "the second one", "the last", or it stays a step of its own. The plan names the part: folded "the errors in eu-west" into 1. Where the part's own words say what to do and the whole sentence reads them as another reflex, the part is a step of its own, and it waits for a yes.
  • What you say you will do yourself is no step. A clause that opens with before or after and has I or we as its subject is set aside where it stands: in "pull the payments logs before I forget", the logs are pulled, and the plan says set aside "before I forget". before you X and after you X still order two steps.
  • A courtesy that opens with if is no condition: if you would, if you don't mind, if possible, if so, if it's not too much trouble. At the head or the end of a part it is set aside the same way.
  • For a part that asks for something and still matches no reflex, the classifier is asked once more which reflex its words ask for, this time reading the whole sentence. Where it picks one, the part is decided as that reflex, its values read from its own words, and becomes a step. Like any step it takes a word or a value the sentence says once, and it always confirms before it runs: in "check the errors for checkout and post in #incident, payments too", the errors for payments are a third step, "payments too" matches nothing alone, and the whole request reads it as errors at 0.85, which always waits for a yes. A part that names a listed word its step cannot take stays unread, and the whole request is refused: read as post, which takes no service, the part shows "payments too" · no reflex.
  • A part that matches no reflex and asks for nothing is set aside, and the plan runs without it: set aside "thanks a lot for this". Where the classifier says the part may add a detail to the step beside it, that step confirms before it runs, its line ending in without "the ones since noon", and the plan names the part: not in the plan "the ones since noon". A part that asks for something and matches nothing, alone or with the whole sentence, still refuses the whole request.
  • Two items the classifier read as one thing, "invoices, card expenses", are two steps when each is a reflex of its own: the joint is split, and a word the sentence says once reaches both. A joint it read firmly as one task stays one. A step made this way confirms where the whole was read as its reflex with a value the step's own call lacks, and names the value's words: "friday" may add a detail this call does not hold.
  • A sentence reads most surely when it asks for a few steps. The plan is settled before anything runs, so the longer a sentence grows, the more often one of its steps stops to ask.

One action for several values

A request can name several values for one action: three people to look up, or two regions to check. evoke plans a step for each value the request asks for. It reads the request for one value first. Then it asks the classifier whether the request asks for each other value as well. Each step is judged alone: one that is sure runs, and one that is less sure waits for a yes.

$ evoke "look up the hires ana, maria and sam"
  1  hire person="ana"  0.91
  2  hire person="maria"  0.87 · with 1
  3  hire person="sam" · read · weakest: route 0.79 · with 1, 2
       a read runs at 0.80 or more
ana: product designer, starts monday 19 october
maria: backend engineer, starts monday 5 october
  3  hire person="sam" · read · weakest: route 0.79
       a read runs at 0.80 or more
  Look up sam's hire?  [y]es [n]o [t]each > y
sam: account executive, starts monday 12 october

A part that asks for the same with another value repeats the step before it, with that value in place. After two different actions, such a part is read on its own, since the same could mean both.

$ evoke "error rate for payments in eu-west, and the same for us-east"
  1  errors service="payments" region="eu-west"  1.00
  2  errors service="payments" region="us-east"  1.00 · with 1
payments in eu-west: 8.4% errors since 14:02
payments in us-east: 8.4% errors since 14:02

Words that point at several things named earlier, like both, become one step for each of them. both names none of them itself, so each of those steps waits for a yes. Its line names the words its value was read from: person was read from "both".

$ evoke "look up ana & maria, order both a pro"
  1  hire person="ana" · read · weakest: route 0.80
       person was read from "ana"
  2  hire person="maria" · read · weakest: person 0.70
       a read runs at 0.80 or more
  3  order person="ana" model="pro" · destructive · weakest: person 0.77
       it cannot be undone, so it always waits for a yes; person was read from "both"
  4  order person="maria" model="pro" · destructive · weakest: person 0.91
       it cannot be undone, so it always waits for a yes; person was read from "both"
  …
  3  order person="ana" model="pro" · destructive · weakest: person 0.77
       it cannot be undone, so it always waits for a yes; person was read from "both"
  Order a laptop for ana?  [y]es [n]o [t]each > y
pro ordered for ana, PO-20463, at the desk by monday 19 october
  4  order person="maria" model="pro" · destructive · weakest: person 0.91
       it cannot be undone, so it always waits for a yes; person was read from "both"
  Order a laptop for maria?  [y]es [n]o [t]each > y
pro ordered for maria, PO-20461, at the desk by monday 5 october

evoke why shows how each step was read. The first step's read apart row says it was read for its own value alone. Each other step was decided on your words, its own value in place of the values you listed: look up the hires sam. Its another value row shows what the classifier answered: whether the request asks for that value as well, means something else by it, rules it out, or wants it instead of the value read. The step's which reflex is the share for asked for as well. For sam it was 0.79, under the 0.80 a read needs, so that step waited for a yes.

$ evoke why
  "look up the hires ana, maria and sam"

  1 · look up the hires ana, maria and sam
    …
    read apart              for the one value it holds: each other value it names is a step of its own after it
    …

  3 · look up the hires sam
    which reflex            hire 0.79
    …
    another value           asked for as well 0.79 · says something else 0.14 · ruled out 0.06 · in place of the one read 0.01
    → ran at your yes: hire person="sam" · read · weakest: route 0.79
      a read runs at 0.80 or more

What a step takes from another

A body may declare what its result holds, under [yields] in its manifest (The manifest). A later step may refer to it with it, them, that report or the address. It takes a field of the right kind into the argument it lacks:

$ evoke "look up dana's address and email them"
  1  contact name="dana"  0.95
  2  mail · takes email from 1
dana <dana@example.com>
  2  mail to="dana@example.com"  0.94
drafted to dana@example.com

mail needed an address the words did not give. contact yields one, and them names it. A required argument is filled with the value. An optional one is decided again with the value written into the words, so the classifier assigns it, under the same gate as any words. A date a step yields reaches the next as a day, 2026-05-06, never as tomorrow: a relative day is nothing to take. A day of the month in a step that takes from a step naming a month and a day is read beside that day, not beside today: in show my calendar for october 12th, then move those meetings to the 20th, the meetings move to October 20th, and the 3rd would be November's. The plan shows the day as read, and evoke why says read beside the day of step 1. Nothing is guessed: a reference that several fields could satisfy, or one record of a list, stops with a line naming them. Within one sentence a step refers to another; across sentences, in one session, a pick with recent recalls what earlier bodies returned, at its ask (Arguments). A step that refers to another whose result it takes nothing from asks before anything runs:

$ evoke "look up dana's address and start a 10 minute timer for them"
  1  contact name="dana"  0.95
  2  timer duration="10 minute"  0.94 · after 1
  step 2 refers to step 1, but takes nothing from it
  Run the plan as it stands?  [y]es [n]o > y
dana <dana@example.com>
10 minute timer started

A step that takes whole results

A step may take earlier steps' results whole. Its manifest names what it takes with takes; each earlier reflex names what it returns with returns (The manifest). The plan binds them by that name before anything runs, whatever the words refer to, and the step waits for the steps it takes from:

$ evoke "check the errors for checkout, list the checkout deploys and pull the checkout logs, then find the suspect"
  1  errors service="checkout"  0.95
  2  deploys service="checkout"  0.95 · with 1
  3  logs service="checkout"  0.95 · with 1, 2
  4  suspect  0.95 · takes errors from 1, deploys from 2, logs from 3
checkout: 8.4% errors since 14:02
checkout: 1 deploy today, 4.12.0 at 13:58
checkout: 412 timeouts calling payments
4.12.0 at 13:58, four minutes before 8.4% errors and 412 timeouts

Steps 1 to 3 run side by side; step 4 receives their three results as its arguments, beside its decision and never in its words, an empty list as a list. A result over 1 MiB is not handed: the step is skipped, and the weave fails as it does when a source yields nothing. When no step before it returns what a step takes, or two steps do, the request stops before anything runs, and says which; so does a field taken from a step that runs once per record:

$ evoke "check the errors for checkout, then find the suspect"
  1  errors service="checkout"  0.95
  2  suspect  0.95 · takes errors from 1
  step 2 takes deploys, which no step before it returns  →  evoke show deploys
  step 2 takes logs, which no step before it returns  →  evoke show logs
[2]

A plan a playbook wrote

A short sentence can reach a playbook, a reflex whose body is a plan of sentences (Playbooks). Its steps stand in the plan as if you had typed them, each decided over what you have installed, and every line ends with the playbook and the step that wrote it. Under the plan comes the sentence's own line, then one question over the whole. Your yes covers every step, and a step's own confirm is asked again at its turn.

$ evoke "checkout is failing in eu-west"
  1  errors service="checkout" region="eu-west"  0.95 · outage 1
  2  deploys service="checkout" region="eu-west"  0.95 · outage 2
  3  logs service="checkout" region="eu-west"  0.95 · outage 3
  4  suspect  0.95 · takes errors from 1, deploys from 2, logs from 3 · outage 4
  5  rollback service="checkout" region="eu-west" · takes release from 4 · outage 5
  6  post channel="#incident"  0.95 · outage 6
  7  status component="checkout"  0.95 · outage 7 · service as component
  outage service="checkout" region="eu-west" · destructive · weakest: route 0.95
    it cannot be undone, so it always waits for a yes
  Run the outage plan for checkout?  [y]es [n]o [t]each > y

A slot the sentence does not state is asked before the plan prints, unless a part of the sentence states it for a step the playbook writes: in "we have an outage, check the errors for payments", the service is payments. A part of your sentence that repeats a step of the plan folds into that step, and the plan says so: folded "show me the error rate" into 1. A part that says what not to do beside such a plan is refused, since the step it may have meant is already there. A sentence that opens with a condition, if, unless or in case, is refused whatever follows: evoke judges no condition, so ask for the check first, then type what to do. A step of the plan that matches nothing here, or that reaches a plan inside a plan inside a plan, refuses the whole plan, and the line names the step. When the sentence holds more than the playbook, the question is Run the plan as it stands?.

A playbook may list steps that run only under a value an earlier step yields, a branch. The plan prints every one before anything runs: the line of the step whose result picks ends then 4 on "yes", 5 on "no", and each alternative's if 3 yields landing "yes". At that step's turn the value picks. The step it picks runs, the others are skipped clean and say so at the end, skipped · not chosen: step 3 yielded landing "yes", and a value no step lists picks nothing, which no step lists, the plan going on (Playbooks).

Before anything runs

The plan is settled first. A step whose required argument no other step provides is asked for it up front, as one input would be at its turn. Then the plan is shown, numbered, one line per step:

$ evoke "kill the lights and start a 10 minute timer"
  1  lights state="off" · asks room
  Which room?  [1] den  [2] office  [+] add one  [0] none of these  > 1
  1  lights room="den" state="off" · write · weakest: state 0.74
       a write runs at 0.90 or more
  2  timer duration="10 minute"  0.95
  1  lights room="den" state="off" · write · weakest: state 0.74
       a write runs at 0.90 or more
  Set the den lights off?  [y]es [n]o [t]each > y
den lights off
10 minute timer started

An answer out of range is asked again with the reason, as one input's would be. Each line of the plan is what evoke would say of that step alone: the call and its confidence, or the confirm's own line with its weakest judgment. After it: asks <arg> for what the step still needs, takes <name> from <n> where a result threads in, the name a field's or a whole result's, after <n> where the words ordered it, with <n> where the step runs beside earlier ones, <playbook> <n> where a playbook wrote the step, then <n> on "<value>" where the step's result picks among later steps, if <n> yields <field> "<value>" where the step runs only under that value, no reflex where nothing matched. Under the plan, folded "…" into <n> names a part that repeats another step, set aside "…" a part that asks for nothing, not in the plan "…" a part that may add a detail to a step, and left out "…" a part that said what not to do.

At each step's turn

Each step then goes through the same gate as one input: a run runs, a confirm prompts, an ask asks. A step that differs from its plan line, a bound value now in place, prints again. In the SDK a step's confirm can wait for someone else: the decision travels as plain data to whoever answers it, and the weave goes on when they do (Decisions). A failure, a decline or a refusal ends the weave after its stage. The steps after it are skipped, and say so:

$ evoke "start a 25 minute timer and kill the lights in the den"
  1  timer duration="25 minute" · write · weakest: duration 0.85
       a write runs at 0.90 or more
  2  lights room="den" state="off"  0.92
  1  timer duration="25 minute" · write · weakest: duration 0.85
       a write runs at 0.90 or more
  Start a 25 minute timer?  [y]es [n]o [t]each > n
  2  lights room="den" state="off"  0.92 · skipped
[2]

A step decided again with a bound value in its words may match no reflex then. It is refused, and says so:

  2  "copy sam@example.com on a note to dana@example.com" · no reflex

A part that matches nothing refuses the whole request before anything runs:

$ evoke "kill the lights in the den and feed the cat"
  1  lights room="den" state="off"  0.92
  2  "feed the cat" · no reflex
[2]

The exit code is the worst step's. 0 every step ran. 1 a body failed, or a step yielded nothing the next could take, or more than it can be handed. 2 a step was declined, a part matched nothing, or a step takes a result no step before it returns, or two do. 3 a step needed a terminal, or a question only you can answer.

Stopping it

Ctrl-C while a step runs stops the weave. The body running at that moment is told to stop and ended with everything it started: a JavaScript body sees its signal abort and has a second to finish. That step and every step after it read skipped · cancelled, on the terminal and in the log, so why and teach still see the whole plan:

$ evoke "wait a while and start a 10 minute timer"
  1  wait  0.95
  2  timer duration="10 minute"  0.95
waiting
^C
  1  wait  0.95 · skipped · cancelled
  2  timer duration="10 minute"  0.95 · skipped · cancelled

Then evoke ends as an interrupted program does, and the shell reports exit 130. Under --json every step's line prints first, its why { "type": "cancelled" }.

try and --json

evoke try shows the plan, then the sentence as it was read: where it was cut, what was folded, set aside or left out, and every step under its number, a playbook's own block first when one wrote the plan (Outcomes). evoke try --json prints the plan whole, on one line, with every adapter call it took. evoke --json prints one line per step as it runs: the line of one decision, with step and steps first, bound where a value came from another step, from where a playbook wrote it, and status at the end, with why when the step stopped; the sentence's own line prints first, as step 0, with no status (The JSON line). A plan stopped before any step ran, refused, its question or its prompt declined, or with no terminal to ask, prints every step's line at once, with what stopped it.

Saving a plan, and running it for someone else

evoke try --save <file> "<sentence>" writes the plan as a file, and runs nothing. The file holds the sentence as you typed it, the plan as try --json prints it, and every answer the classifier gave, in clear; and the pins the plan was decided under: the evoke version, the adapter and its gate, the installed set's digest, each reflex and vocabulary by its own hash, a remote reflex by the lock's h1. It carries no time and no name. A plan that asks cannot be saved: a file holds no one's answers, so the sentence must say the value, kill the lights in the den rather than kill the lights. The name <name>.plan.json is recommended, not required.

$ evoke try --save ~/month.plan.json "pull september's bank transactions, invoices, card expenses and payroll, reconcile them, post the closing entries to the ledger, then send the report to cfo@example.com"
  1  bank month="september"  0.95
  2  invoices month="september"  0.95
  3  cards month="september"  0.95
  4  payroll month="september"  0.95
  5  reconcile  0.95 · takes transactions from 1, invoices from 2, expenses from 3, payroll from 4
  6  ledger month="september" · write · weakest: route 0.89
       a write runs at 0.90 or more
  7  send to="cfo@example.com"  0.95 · after 1, 2, 3, 4, 5, 6
+ ~/month.plan.json

evoke run <file> runs it, on your machine or another with the same project: the same reflexes at the same content, the same words, the same adapter and gate. run tells a file from a call by its first word, which is no reflex name. The plan is made again from the file's answers alone, so the classifier is asked for nothing, and a plan that does not read the same as the file is refused. The plan prints, one yes over the whole plan is asked at your terminal, whatever its verdict, then each step goes through the gate at its turn as any sentence's does: a confirm asks you, a value bound at run time is decided again by your adapter under the same gate. Every step's line in your log names the file, and evoke why shows it as from ~/month.plan.json.

$ evoke run ~/month.plan.json
  1  bank month="september"  0.95
  …
  7  send to="cfo@example.com"  0.95 · after 1, 2, 3, 4, 5, 6
  the plan of ~/month.plan.json
  Run the plan as it stands?  [y]es [n]o > y
september: 214 transactions, 18 204.55 at the close
…
  6  ledger month="september" · write · weakest: route 0.89
       a write runs at 0.90 or more
  Post september to the ledger?  [y]es [n]o [t]each > y
september posted to the ledger
sent to cfo@example.com

A pin that moved refuses the file with one line naming it, exit 3, and the command that mends it: a plan written by another evoke, decided by another engine or under another gate, a reflex not installed here or inactive here, a remote reflex whose body moved, a reflex or a vocabulary whose wording moved, a reflex installed here the plan never saw, or the project's files differing at all. A file edited by hand does not read the same as its answers, and is refused too. The fix is to make the plan again here and read it again:

$ evoke run ~/month.plan.json
  months differs here from the plan's  →  evoke try --save ~/month.plan.json "pull september's …"
[3]

In the SDK, steps returns the same object and weave takes it (Decisions).

Afterwards

Every step is logged under its number. evoke why shows the last sentence as it was read, each step with what became of it — ran, failed, declined, refused, skipped or unanswered, and why, cancelled among the reasons — a playbook's own block first when one wrote the plan, and under a value the step it was taken from or the part of the sentence that gave it. evoke teach <call> with no utterance takes the step the lesson's reflex decided; when none or several did, it names the steps and asks you to say which (Tuning).

Next: Installing reflexes.

Installing reflexes

Git is the registry. A reflex lives in a repository, at a version tag. evoke add fetches it, pins it in a lock, and keeps its code in a store keyed by content. It runs the code only while it still hashes to the lock. Nothing is installed that a person did not name. No install-time code ever runs.

Refs

FormMeans
owner/repoEvery reflex directory in the repository, on GitHub
owner/repo/dirOne reflex, the directory dir of the repository; dir may nest
owner/repo/dir@1.2.0Pinned: update keeps it there. Tags are X.Y.Z or vX.Y.Z. The newest is the highest
https://host/repo.git#dir@1.2.0Any git host over https or ssh. #dir and @tag are optional
ssh://git@github.com/owner/repoThe same over ssh, the user before the host
./dir, ../dirA local directory, relative to evoke.toml. Never fetched, never locked; one outside the project is shown where it is, ~/hello

An unpinned ref means its newest tag at add, and update moves it forward. A repository with no version tag cannot be installed. Its author publishes one with git push --tags. A password or a token in a URL is refused: git's credential helper holds those, never a project file.

add

$ evoke add radhi/home/lights radhi/timer
+ lights  radhi/home/lights 1.2.0  write  runs lights.mts
+ timer   radhi/timer 1.0.1        write  runs timer.mts
  inactive  lights: vocabulary "rooms" is empty  →  evoke vocab rooms add <word> "<meaning>"
  inactive  lights: config "bridge" is not set   →  evoke config lights bridge <value>
  inactive  lights: config "token" is not set    →  evoke config lights token --env <VAR>

A reflex whose manifest declares what its body touches shows it on a second line under its row, needs writes {to} ~/Downloads · hosts *, as the first ten minutes show; a row without one declares nothing.

evoke add <ref>… [--as <name>] does, in order:

  1. Fetches each ref at its pin or its newest tag. This is a bare, shallow fetch, read without a checkout, once per repository and tag. Symlinks and submodules are refused. The tree is kept in the store. A local ref, ./dir, is read where it is and written to evoke.toml as a path relative to that file. It is never fetched and never locked.
  2. Reads and lints every manifest. A manifest that does not parse refuses the whole add. Nothing is written until every newcomer is in hand. Lint only reports; it never refuses. It flags a summary over 100 characters, a description over 1 000, more than eight not_for entries, more than 24 options, more than 40 records per table, an utterance over 200 characters, or text that addresses a model instead of describing an action.
  3. Tests for theft. The examples already installed are routed over the new set, a few at a time, each newcomer's fit asked too. A phrase a newcomer wins prints as <thief>: steals "<phrase>" from <owner> → evoke teach "<phrase>" not <thief>; one it fits over the floor, so that two reflexes claim the phrase, as <thief>: also fits "<phrase>" of <owner> (0.82), with the same fix. The add still proceeds. The fix is one line in your overlay. When the classifier cannot answer, or has no key yet, the line says the test did not finish, and evoke test runs it again.
  4. Writes the [reflexes] lines to evoke.toml, the lock, and evoke.d.ts. It records the JavaScript runtime when a newcomer runs a file. Then it prints one row per newcomer behind +, then the lint lines, the theft lines, and each newcomer's inactive lines.

The local name is the ref's last segment. --as <name> picks another, for a single ref. The local name is what the classifier reads, and your overlay file is named after it. A name already taken is refused. For one ref, the fix is the --as line; for a collection, the line that adds the rest, or evoke update when every reflex of it is installed already. So is the same reflex under another name, the one the lock already holds by content: clock: is timer under another name → evoke show timer.

remove

evoke remove <name> drops the [reflexes] line and the lock entry, and prints the row behind -. Your overlay, your vocabularies and your settings stay, because they are yours. The store's copy stays too; it is a cache.

update

$ evoke update
  lights 1.2.0 → 2.0.0  major · code changed
    description  rewritten, taken (you have no override)
    args.state   renamed power; your 1 example follows

evoke update [<name>] moves every unpinned remote reflex, or just one, to its newest tag. A pinned reflex moves to its pin. It never prompts, never blocks, and never rewrites your files. When nothing moves, it prints up to date. A reflex whose new manifest does not read is reported at its new tree in the store and skipped; the rest move, and the exit is 3. For each move, it prints the level of the change and one line per detail:

LineMeans
same, minor, majorWording only, or a declaration narrowed; additions, a declaration widened among them; something your files or calls may not survive
· code changedA file other than the manifest changed
description rewritten, takenYou have no override, so upstream's new text is used. Otherwise: yours kept
args.state renamed power; your 1 example followsThe author declared the rename; your records follow it at merge time
staleYou override something upstream changed: yours wins, both are shown once
orphanedA line of yours addresses nothing any more: skipped, the rest applies
effect tightened to writeUpstream tightened the effect; it applies
effect write upstream; destructive kept until you acceptUpstream loosened it; you keep what you consented to
needs narrowed: hosts * droppedUpstream narrowed what the body touches; it applies
needs widened: writes ~/notes upstream; none kept until you acceptUpstream widened it; you keep what you consented to

The lock holds the effect and the declaration you consented to. Upstream may tighten either at any time. They loosen only through evoke update --accept <name>, which takes the looser effect and the wider declaration at the current tag and says so: effect write accepted; was destructive, needs writes ~/notes accepted; was none. A new reflex in a repository you already use is reported once, with its add line. update never adds one for you.

sync

On a new device, or in CI, you have the lock and the store is empty. evoke sync fetches each repository once, at every locked tag it needs, checks that each tree hashes to the lock, and records the runtime. It prints a + row per reflex placed, or up to date. It never changes the lock. A tag that moved or vanished is refused, with evoke update <name>. Before sync, evoke show lists what the lock names, each missing tree as an inactive line. A git call that has not finished within a minute is a failure, never a hang.

$ evoke sync
+ lights  radhi/home/lights 1.2.0  write  runs lights.mts

trust

Your home project is trusted by construction, and evoke trust there says so. Any other project, an application's or a clone's, must be trusted before evoke decides in it. A project's files decide what runs:

$ cd app && evoke "kill the lights in the den"
  ~/app is not trusted  →  evoke trust
[3]
$ cd app && evoke trust
+ trusted ~/app

Trust binds to the content of the four owned paths: evoke.toml, evoke.lock, overlays/, vocab/. evoke's own writes renew it. Any other edit, yours or a git pull's, makes the next run say changed since you trusted it → evoke trust. Trust lives in ~/.local/state/evoke/trust.toml, on this machine only.

The lock and the store

evoke.lock records, for each remote reflex, the ref, the tag, the commit, the content hash h1 of the reflex directory, and the effect and the declaration you consented to. It also records the adapter the project decides with, by name and id. Commit it with your project.

Fetched code lives under ~/.cache/evoke/store/<h1>/. It is re-hashed against the lock every time evoke starts. A store entry that does not match is a miss, and evoke sync places it again. Your project directory never holds code you did not write.

The runtime

A reflex that runs a .mts or .mjs file needs Node 24 or newer. add and sync find node on your PATH and record its path in ~/.local/state/evoke/runtime. Nothing is searched at run time, so a cron job's PATH cannot break a decision. If none is found and none is recorded, the command ends with node is not on PATH; a .mts reflex needs Node 24 or newer → evoke sync and exit 3, after its rows. Install Node, and evoke sync records it. An argv reflex needs no runtime.

Next: Tuning.

Tuning

A reflex ships its wording. You own the last word. Every change lands in a file under your project. evoke writes it with your comments and order kept, and every later decision reads it. No command here needs the classifier's key but test, which asks it.

show: what is installed, and one reflex as used

$ evoke show
  lights  radhi/home/lights 1.2.0  write  runs lights.mts
  timer   radhi/timer 1.0.1        write  runs timer.mts
  inactive  lights: HUE_TOKEN is not set  →  export HUE_TOKEN=<value>

evoke show <name> prints the reflex's effective manifest: the shipped one with your overlay merged in. It prints TOML, one key per line, with + next to every line that is yours: A line of your overlay that addresses nothing the reflex has, an argument it dropped, is listed after the manifest as skipped, as update reported it.

$ evoke show lights
  description = "Turn the lights in one room on, off, or dim them.\nCeiling and lamp lights only."
  …
  [args.state]
+ ask = "On, off or dim?"
  options.on = "Switch on."
  …
  [examples]
  "turn on the kitchen lights" = { state = "on" }
+ "kill the lights" = { state = "off" }

teach: one line in your overlay

$ evoke teach "kill the lights" lights state=off
+ overlays/lights.toml  [examples] "kill the lights" = { state = "off" }
$ evoke teach "what time is it" not timer
+ overlays/timer.toml  [examples] "what time is it" = false
$ evoke teach lights state=dim
+ overlays/lights.toml  [examples] "dim the office" = { state = "dim" }
  • evoke teach "<utterance>" <call>: the utterance means this call. The call uses the same grammar evoke prints, name arg=value…. Only what you assert is recorded. lights state=off teaches the route and state, and leaves room to the input.
  • evoke teach "<utterance>" not <name>: the utterance is never this reflex.
  • evoke teach --forget "<utterance>" <name>: the line your overlay holds for the utterance, an example or a test, is removed; the shipped wording stands again. One not there is refused.
  • A phrase the reflex's examples already hold with the same values is said to be one, and nothing is written.
  • Leave the utterance out, and it means the last input you gave. Say something, see it decided, then evoke teach lights state=dim corrects it. After a sentence of several steps, it means the step the lesson's reflex decided; when none or several did, teach names the steps and asks you to say which.
  • A one-word utterance needs no quotes: evoke teach dim lights state=dim. The first word is the utterance when a call follows it, unless the word names an installed reflex. Then the call begins with it.
  • [t]each at a confirm prompt does the same for the input just decided.

A value is checked at the door. A word the vocabulary lacks, an option not offered, or an argument the reflex does not have is refused, with the command that shows what is allowed.

Overlays

overlays/<name>.toml is your wording for one reflex, in the manifest's own shape. It is merged over the shipped one by one rule: tables merge by key, and every value replaces whole. An overlay adds and replaces. It never deletes.

# overlays/lights.toml
description = "Turn the lights in one room on, off, or dim them."      # replaces the shipped text whole

[args.state]
ask = "On, off or dim?"                                                # new wording for an argument
options.dim = "Lower the brightness; the lights stay on."              # new wording for an existing option

[examples]
"kill the lights" = { state = "off" }                                  # sent to the classifier

[tests]
"turn on the kitchen lights" = { state = "on" }                        # a shipped example, moved here: held out now

What you may change is wording: description, not_for, tags, confirm, each argument's ask, the meaning of its existing options, examples and tests. What you may not change is the contract: what the reflex runs, its argument names, option keys, sources and ranges. A contract key in an overlay makes the reflex inactive, and names the line to remove.

  • The highest layer that names an utterance decides its table and its value. Move a shipped example under [tests], and it is no longer sent. = false rejects it.
  • effect in your overlay may tighten: write over read, or destructive over either. It may never loosen. A loosening line makes the reflex inactive.
  • Lists, like not_for and tags, replace whole.
  • Your records may assert vocabulary arguments, like { room = "den" }. Shipped manifests may not.
  • Wording in another language is just an overlay.

At update, an overlay follows a declared rename. evoke reports what went stale: you override something upstream changed, and yours wins. It reports what is orphaned: a line of yours addresses nothing now, and is skipped. An overlay that fails to parse makes its reflex inactive, never silently looser.

Vocabularies

vocab/<name>.toml is a closed list of your words. Every argument that names the vocabulary shares it. A package never ships or writes one. A reflex whose required argument reads an empty vocabulary stays inactive until you fill it; an optional one is simply never stated.

# vocab/rooms.toml
den    = "The TV room downstairs; also 'the snug'."
office = { what = "The upstairs study.", value = "group-7" }

The meaning is what the classifier reads, so other names for the thing help. The value, when set, is what the program receives instead of the word. The classifier never sees it. It can be a path, a URL, or a device id.

$ evoke vocab rooms
  den = "The TV room downstairs; also 'the snug'."
  office = { what = "The upstairs study.", value = "group-7" }
$ evoke vocab rooms add attic "The loft upstairs." --value group-9
+ vocab/rooms.toml  attic = { what = "The loft upstairs.", value = "group-9" }
$ evoke vocab rooms remove attic
- vocab/rooms.toml  attic

add of a word already there replaces its meaning. A word may contain spaces. none and unstated are reserved. At an ask over a vocabulary, + adds a word without leaving the prompt, its path too when a reflex declares the word's value as one it reads or writes.

config: settings and secrets

A manifest declares under [config] what it needs from you. Each setting lands under [config.<name>] in evoke.toml. A reflex with a setting unset stays inactive.

$ evoke config lights bridge 10.0.0.2
+ evoke.toml  [config.lights] bridge = "10.0.0.2"
$ evoke config lights token hue-example
  lights: config "token" is a secret; it is set from a variable  →  evoke config lights token --env <VAR>
[3]
$ evoke config lights token --env HUE_TOKEN
+ evoke.toml  [config.lights] token = { env = "HUE_TOKEN" }

A setting a declaration reads or writes as a path, writes = ["{file}"], is looked for: one that is not there yet is said, and the body waits for it. A secret is only ever set with --env <VAR>. Your file names the variable. The value reaches the program from the environment at run time, and nowhere else. --env works for any key. An undeclared key is refused, with evoke show <name>, which lists what the reflex declares.

test: every record, judged

$ evoke test
  lights  3 passed · 1 failed
    "make it darker in here"  state: expected "dim", read "off"
  timer   8 passed
    "ping me in 2 hours"  passes, but would wait for a yes: the call holds all of it at 0.20, and a call runs at 0.30 or more
  1 of 12 cases failed  →  evoke test
[1]

evoke test [<name>] decides every example and every test of every active reflex, or of one. It decides against the whole installed set, a few cases at a time, never through the cache. It judges each case on its route and its asserted arguments. It exits 1 when a case failed, so a script can act on it. It never blocks an install. A case that passes while its call would wait for a yes, holding less than the case's own words, is named under its reflex and fails nothing (Records).

A case typed in a language other than English puts a line before the reflexes, one per such language, with how many of its cases passed and failed (Languages). A case that passed last time and fails now is decided twice more. Failing two of three marks it · regression. The last verdicts are kept per installed set, on this machine. What the confidence beside each call meant on the same records is evoke calibrate's answer: Calibrating.

Thresholds

Everything engine-specific lives under the adapter's own table, so one dotfiles repository serves machines on different engines:

[adapters.jev]
gate = { route = 0.5, read = 0.85, write = 0.95, whole = 0.4 }

The openjev adapter takes the same table under [adapters.openjev]. Both reach Jev, so the numbers mean the same. Each number means the probability this is right. read may not exceed write. whole is the third bar: a call that cleared its effect's bar is read back against your sentence, and runs when it holds all you typed at whole or over; raise it and more calls wait for a yes with the words they leave out named, lower it and fewer do. Destructive reflexes always confirm. Outcomes explains what the numbers gate, and Calibrating measures each bar on your own records.

The clef and clef_flash adapters reach Clef, Cloudflare's model. They ship no bars, so every call through them waits for a yes. To let calls run, give all three of route, read and write in the gate of [adapters.clef] or [adapters.clef_flash]. whole and fits are yours to add. Choose the numbers from your own records, which evoke calibrate measures. Environment lists the two variables these adapters read.

Next: Calibrating.

Calibrating

Every call prints a number: lights room="den" state="off" 0.85. It claims that a call like this is right about 85 times in 100. evoke calibrate checks the claim against the records you own, and prints what it found. It needs the classifier's key, or a recording to answer from (below).

What the number claims

The number is the lowest of the probabilities the classifier gave the route and each value the call holds (Outcomes). So the one event it can be measured on is the whole call is right: the route, and every argument the record names. A record is an example or a test of a reflex (Examples and tests). calibrate decides every record of every active reflex once more, never through the cache, against the whole installed set, the whole reading of each; a sentence that is several records is decided once. It judges each record as evoke test judges it. Then it counts whole calls by the confidence they claimed.

The report

Over the manual's own lights and timer records, on a recording, after three decisions were made and logged:

$ evoke calibrate
  replay · 12 records over 2 reflexes · 12 inputs, decided once
  7 run · 3 ask · 2 abstain
  whole call right, by confidence
    0.80–0.90  1 call    right 0 ·   0% (0–79) · claimed 0.80 · over-confident · thin
    0.90–1.00  9 calls   right 9 · 100% (70–100) · claimed 0.95 · thin
    abstained  2 inputs  right 2 · 100% (34–100)
  wrong at or over write 0.90   0 of 6 · 0 per thousand, at most 393 · thin
  near write 0.90   at 0.80 6 run, 0 wrong · at 0.85 6, 0 · at 0.90 6, 0 · at 0.95 5, 0 · at 1.00 0, 0
  wrong at or over whole 0.30   0 of 6 run · 0 per thousand, at most 393 · 1 of 7 held for a yes, 1 right · thin
  each judgment on its own
    route   12 judgments  right 100% (76–100) · claimed 0.95
    options  3 judgments  right  67% (21–94) · claimed 0.90
    pick     9 judgments  right 100% (70–100) · claimed 0.95
  Brier 0.066 · reliability 0.066 · resolution 0.090
  misses
    "make it darker in here"  ask lights 0.80 · state: expected "dim", read "off"
  the log · 3 decisions under replay · 1 ran · 1 confirmed and stopped · 1 abstained
    stopped at confirm, by confidence   0.80–0.90 1
    3 were records · right 2 · 67% (21–94)
  thin: under 100 calls in every bin, nothing proven at any bar

A record typed in a language other than English adds a block, by language pack, with a line per language that read one: how many inputs it read, and how many of their whole calls were right and wrong (Languages).

Line by line:

  • The head counts the records, the reflexes they belong to and the distinct inputs, and says how many times each input was decided. Then how many inputs ran, confirmed, asked or abstained; an outcome none reached is left out.
  • Whole call right, by confidence. One line per bin: its range, the calls in it, how many were right, that share in percent with its 95 % interval, and the confidence the calls claimed on average, as a probability. The bins' edges sit at the bars, 0.50, 0.80 and 0.90, so no bin straddles one; a range with a few hundred calls splits into bins of about a hundred; a bin with no call is left out. thin marks a bin under a hundred calls: it is shown, and it proves nothing. over-confident marks a bin whose claim is above its interval. A record of false, which says never this reflex, routed to a reflex no record names is unknown, counted apart. The abstains have a line of their own: an abstain is right on a false record, wrong on any other.
  • Wrong at or over a bar. For each effect with a call, the complete calls at or over its bar — an ask never runs, so it is not among them — how many were wrong, the rate per thousand, and the most it could be at 95 %. This is the number a script gates on. Under it, the neighbourhood: at the bar and two steps of 0.05 either side, how many calls would run and how many of those are wrong.
  • Wrong at or over whole. The calls over the bar of their effect are read back against the input (Outcomes). This line counts those the third bar lets run, how many of them were wrong, per thousand with its bound; then what the bar costs: how many it held for a yes, and how many of those were right.
  • Each judgment on its own: the route, then the arguments by the source of their values, options, a vocabulary, a pick or a flag, each judged by its own probability where a record names it.
  • Brier, the mean squared distance between each claim and the truth, with its reliability, how far the claims sit from the shares, and its resolution, how far the bins' shares sit from the whole; --json carries the uncertainty too.
  • The misses: each record a decision missed, what was decided, and where it missed, as evoke test says it.
  • The log: every line of ~/.local/state/evoke/log.jsonl under this adapter, counted by what became of it: ran, confirmed and ran, confirmed and stopped, asked and stopped, abstained, failed. The confidence of what stopped at a confirm, by range. The lines whose input is a record, judged by it.
  • The last line says thin while every bin is under a hundred calls. With a hundred calls in some bin, it says whether any such bin is over-confident at 95 %.

Nothing is said by colour alone: thin, unknown and over-confident are words.

What a record proves, and what the log does not

A record proves one call. An asserting record says whether the route and every argument it names are right; an argument it does not name is not compared. A false record proves only not this reflex: routed to its own reflex the call is wrong, abstained it is right, routed to a third reflex it proves nothing, unless another record of the same sentence names that reflex. Examples are sent to the classifier, so a right example shows less than a right test, which the classifier never saw. The report counts both.

The log's lines are weaker. A run that a person watched says nothing about the call. A confirm answered yes says a person read the call and agreed. A confirm without a result says no, or no answer, or no terminal: the line cannot tell. So the log's block counts what became of each decision, and never mixes into the bins above. A line whose input is a record is judged by the record, which settles it.

--repeat

evoke calibrate --repeat 3 decides each input three times, a few at once, never through the cache. Every number above still counts each input once, by its first decision: the repeats measure stability, not accuracy. They add one line, and under it the inputs that moved most:

  repeats 3 · winner flips 0 of 12 · verdict flips 0 · spread median 0.00, 90th 0.00, max 0.00 · straddling a bar 0 · wrong at or over a bar 0

A winner flip is an input whose route came out differently across repeats; a verdict flip, one whose call was right in some repeats and wrong in others. The spread is the range of the confidence over the repeats, per input. An input straddles a bar when some repeats sat under the bar of its effect and others at or over it. Wrong at or over a bar counts the calls, in any repeat, that were wrong there. Under the line, each input that moved: its confidence and route ranges, its winners and outcomes with their counts, and how many repeats were wrong. A miss says how many of the repeats missed the same way. On a recording the spread is nothing, as above; against a live engine, the report says how far the same sentence moves from one decision to the next.

Ten repeats over a thousand records is ten thousand requests: minutes, and the key's cost.

A recording, in CI

The report needs no key over a recording. Set adapter = "replay" in evoke.toml and the recording's path in EVOKE_ANSWERS, as the SDK's Testing page shows, and the same report runs offline, the same every time. That is how this manual's block above was made.

--json

evoke calibrate --json prints the report as one object: adapter, records, reflexes, inputs, repeats, outcomes, bins, unknown, abstained, bars with read, write and whole, questions, brier, misses, variance under --repeat, and log: The JSON line.

The exit

calibrate exits 1 when a call was wrong at or over its bar, in any repeat, or when a bin of a hundred calls is over-confident at 95 %, with the reason on its last line:

  1 call wrong at or over its bar  →  evoke calibrate
[1]

Otherwise it exits 0, however thin the bins. Nothing to calibrate, no active reflex with examples or tests, says so in one line. Nothing runs, and nothing is logged.

Next: Projects.

Projects

A project is a directory of files you own. evoke reads the nearest one and writes only inside it.

~/.config/evoke/   or   <app root>/
├── evoke.toml            yours: the adapter, the reflexes, their settings
├── evoke.lock            written by add, update and remove: what exactly is installed
├── evoke.d.ts            written with the lock: types for an app's decisions
├── overlays/lights.toml  yours: your wording for one reflex, named by its local name
└── vocab/rooms.toml      yours: your words

Which project

The CLI looks for the nearest evoke.toml, upward from the working directory. If there is none, it uses your home project in ~/.config/evoke/ (or $XDG_CONFIG_HOME/evoke/). First use writes the home project with its one line, adapter = "jev". Any project but home must be trusted before evoke decides in it.

The SDK takes an explicit root and never searches: Projects in code.

evoke.toml

adapter = "jev"                    # who decides; a name, never a path; always stated

[reflexes]                         # local name = where it comes from
lights = "radhi/home/lights"       # unpinned: update moves it forward
timer  = "radhi/timer@1.0.1"       # pinned
hello  = "./hello"                 # local, relative to this file: never locked

[config.lights]                    # what the reflex's [config] declares
bridge = "10.0.0.2"                # a value, stored plain
token  = { env = "HUE_TOKEN" }     # a secret: the variable, never the value

[adapters.jev]                     # everything engine-specific, under the adapter's name
gate = { write = 0.95 }
  • A local name matches [a-z][a-z0-9_]*. none, unstated, fits and weave are reserved. The local name is what the classifier reads, and the overlay file is named after it.
  • The built-in adapters are jev, TypeSafe AI's own API, openjev, the same model through OpenJEV, an independent service, and replay, a recording for tests. The first two each read their own key and take the same gate table under their own name: Install.
  • Two more adapters, clef and clef_flash, reach Clef, Cloudflare's model, on its Workers AI service. They read a token and an account id, and ship no bars of their own: Thresholds.
  • An adapter name resolves only against evoke's built-ins, never from the project directory. An adapter sees every input, so that matters. A table for an adapter you did not select is inert. So one file serves devices on different engines.
  • Edit the file by hand if you like. evoke validates it whole and names the line to fix. Its own writes keep your comments and order.

Safe for dotfiles

A project holds only what you wrote, the lock and generated types. No fetched code, no secrets, no cache. Commit it. On a new machine, evoke sync rebuilds the store from the lock and records the runtime. Decisions run the same.

Machine-local state lives outside the project, under XDG. The store and the answer cache are in ~/.cache/evoke/. The log, the trust file, the runtime path and the REPL's history are in ~/.local/state/evoke/. The full list: Environment.

An application's project

An app keeps its project at its root and reads it with the SDK. evoke add next to the app writes the same three files, and evoke.d.ts types the app's decisions by reflex. Overlays and vocabularies under that root tune the app's wording, just as your home project tunes yours. The SDK can also compile the same project over a tenant's own vocabularies, with with({ vocab }). So a multi-tenant server keeps each tenant's words in their own plan.

Local reflexes

hello = "./hello" names a reflex directory next to the file. It can be an author's own, or one you keep out of git. It is never locked. Its content is hashed at run, and its effect is what its manifest says. The store is not involved.

Trust, briefly

Trust binds a project's four owned paths to their content. evoke's own writes renew it. Any other change asks for evoke trust again. Home is trusted by construction. Why this matters: Security.

Next: the Author section, or the Reference.

Your first reflex

A reflex is a directory: a manifest and the file it runs. evoke new writes a working one. evoke check keeps it honest. A project next to it lets you try it before anyone installs it.

1. Make it

$ evoke new hello
+ hello/reflex.toml
+ hello/hello.mts
+ hello/reflex.d.ts

Three files, from a template that already works:

# hello/reflex.toml
reflex = 1

description = """
Say hello to someone.
The name is the text in quotes, "like this"; the greeting is printed and nothing changes."""
not_for = ["saying goodbye"]
effect = "read"
confirm = "Say hello to {who}?"
run = "hello.mts"

[args.who]
ask  = "Who should be greeted?"
pick = "quoted"

[examples]
'say hello to "Ada"'   = { who = "Ada" }
'greet "Grace"'        = { who = "Grace" }
'a hello for "Edsger"' = { who = "Edsger" }

[tests]
'wave at "Linus"'      = { who = "Linus" }
"say hello"            = { who = false }
"bid everyone goodbye" = false
// hello/hello.mts
import type { Reflex } from "./reflex.d.ts"

export default (async ({ who }) => `Hello, ${who}!`) satisfies Reflex

reflex.d.ts is generated from the manifest. It holds Args, with who: string, and the Reflex type that checks the body's arguments and its return. The body imports that one file and nothing else.

For a playbook, a reflex whose body is a plan of steps, evoke new --playbook <name> writes reflex.toml alone:

$ evoke new --playbook drill
+ drill/reflex.toml

2. Check it

$ cd hello && evoke check
  hello  read  runs hello.mts

evoke check reads the manifest and names every line to fix. It confirms that the file run names exists, loads, and exports a function by default. It reports lint. When the arguments changed, it rewrites reflex.d.ts and prints + reflex.d.ts. Inside a git repository with a version tag, it also diffs the contract against that tag: Publishing.

$ evoke check
  broken: run: "nothing.ts" must end in .mts or .mjs  →  ~/broken/reflex.toml:4:1
[3]

3. Try it

A reflex is decided on inside a project. Keep one next to your reflexes while you work: a file naming the adapter, and evoke add ./hello writes the rest.

~/dev/
├── evoke.toml
└── hello/
# ~/dev/evoke.toml
adapter = "jev"

A project outside home must be trusted once. Edits to the reflex itself never need it again. Trust binds to the project's own files, not to the reflexes.

$ cd ~/dev && evoke trust
+ trusted ~/dev
$ cd ~/dev && evoke add ./hello
+ hello  ./hello  read  runs hello.mts
$ cd ~/dev && evoke try 'say hi to "Ada"'
  "say hi to \"Ada\""

  which reflex            hello 0.97   (none of them 0.03)
                          Say hello to someone.
  who                     "Ada" 0.98   (not said 0.02)
  holds all you said      0.90
  → runs: hello who="Ada"  0.97
    a read runs at 0.80 or more, and its least sure judgment is which reflex, 0.97; it holds all you said at 0.90, and a call runs at 0.30 or more

  replay answered 5 questions in 2 rounds
$ cd ~/dev && evoke 'wave at "Grace"'
  hello who="Grace"  0.96
Hello, Grace!

evoke test hello runs the manifest's own examples and tests against the classifier, and reports every miss. evoke run hello who=Ada calls the body without the classifier at all.

4. Make it yours

From here, three pages carry the rest: the manifest key by key, the four kinds of argument, and what a body receives and returns. Then the rules, which say how to write a manifest so that what people type reaches your reflex. And publishing, which is a git tag.

This page took you through the first steps of working on a reflex. Rule 45 lists every step, in the order that finds each fault first. Read your reflex against the checklist before its first tag.

Next: The manifest.

The manifest

reflex.toml is what the classifier reads and what the body receives. It has no name and no version. A reflex's name is whatever each user installs it as. Its version is the git tag it was fetched at.

This page says what each key is and what evoke does with it. Rules for reflexes, playbooks and collections says how to write each one so that what people type reaches the right reflex.

The whole shape

reflex = 1                                   # the format's version; frozen

description = """
Turn the lights in one room on, off, or dim them.
Ceiling and lamp lights, through the Hue bridge; their colours and schedules stay as they are."""
not_for = ["colour scenes and schedules", "asking whether a light is on"]
tags    = ["home", "lighting"]
effect  = "write"                            # always written; absent means destructive
confirm = "Set the {room} lights {state}?"
run     = "lights.mts"                       # or argv: ["hue", "set", "{room}", "{state}"]

[needs]                                      # what the body touches; absent means its own directory and TMPDIR
hosts = ["*"]                                # the bridge is on the network

[config]
bridge = "Hue bridge address"
token  = { about = "Hue API key", secret = true }

[args.room]
ask   = "Which room of the house?"
vocab = "rooms"                              # options come from the user

[args.state]
ask = "What should the lights do?"
options.on  = "Switch on."                   # options come from the author
options.off = "Switch off."
options.dim = "Lower the brightness without switching off."

[args.brightness]
ask      = "How bright, in percent?"
pick     = "number"                          # options come from the input
range    = [1, 100]
optional = true

[yields]                                     # what the returned data holds, for a later step
level = "number"

[examples]                                   # sent to the classifier
"turn on the kitchen lights"   = { state = "on" }
"dim the office to 30 percent" = { state = "dim", brightness = "30 percent" }
"bedroom lights off"           = { state = "off" }

[tests]                                      # held out, never sent
"make it darker in here"           = { state = "dim", brightness = false }
"flip the hall lights"             = { state = false }
"is the porch light still on"      = false
"set the lounge to a sunset scene" = false
"light a candle"                   = false

After reflex = 1, the keys are written in the order evoke show prints them, which is the order of the table below. Within an argument, ask comes first and its source next, then range, recent, optional and was. The arguments with takes come after the others. In TOML a plain key written below a table header belongs to that table, so the plain keys come first: rule 36.

Key by key

KeyRequiredMeaning
reflexyes1. The format is frozen. Keys are never removed or given new meanings, so every manifest ever tagged stays readable
descriptionyesWhat the reflex does. The first line is the summary, never empty. The classifier reads all of it, not the summary alone. The summary says the action and what it acts on, and the next line draws the boundary: rule 11
not_fornoWhat it does not do, one line per entry. It is read as the no side of "does this reflex do what was asked?". It names the near neighbours: rule 13
tagsnoWords for --tag to narrow a decision by. No other meaning. [a-z][a-z0-9_]*
effectalwaysread, write or destructive. It sets how sure evoke must be before a call runs unasked, and a destructive call asks for a yes every time. Left out, it still means destructive, and lint reports effect is absent, which means destructive; write it. It is the author's claim, and not trusted: a user may tighten it, never loosen it. A reflex has one effect. Claim the one the action has: rule 33
confirmyesThe one-line question a person answers. A {placeholder} names a required argument. A pick shows its span. evoke check refuses a placeholder for an optional argument, a flag or a taken result. The confirm reads the call back: rule 23
runone ofThe body: a path ending in .mts or .mjs inside the directory, or an argv. The body
stepsone ofA plan in place of a body: one sentence per step, {slot} for an argument, [ words with a {slot}] for an optional one, { say = "…", when = { field = "value" } } for a step that runs only under what the step before it yields; at most 24. None of a body's keys with it. Playbooks
platformsnoWhere the body runs, when not anywhere: ["macos"], ["linux"], or both. On another machine the reflex is inactive, and add and show say so: runs on macOS only
returnsnoThe name of what the body's data is, whole, for a later step to take: returns = "deploys". Not with an argv run. A result another step takes whole
[needs]noWhat the body touches, held by the kernel: reads, writes, hosts, runs. Left out, the tightest declaration: its own directory and TMPDIR. What the body touches
[config]noSettings the user provides with evoke config: key = "about" or key = { about, secret = true }. A secret is only ever set from an environment variable
[args.<name>]noThe arguments: an ask and exactly one source, or takes alone. Arguments
[yields]noWhat the body's data holds, for a later step to take (Weaving) or for a pick's recent to recall (Arguments): per field, the recognizer that reads it, number, duration, email, url, quoted, date, time, amount or code; or { each = { … } } for a list of records. Declare each field a later step takes, by the noun a person would type for it: rules 30 and 31
[examples]noUtterances with what they assert, sent to the classifier. Examples and tests
[tests]noThe same shape, held out: never sent, run by evoke test

A result another step takes whole

A body may name what it returns: returns = "deploys". The name says what the body's data is, as a whole. A later step whose argument has takes = "deploys" receives that data as the body returned it (Arguments). Two reflexes meet when their names are equal, as two arguments share a vocabulary by naming it. Give each whole result one noun and one shape across the collection: rule 32. A reflex never returns a name it takes: a step that refines a result returns a new name, so a later step has one source. An argv body returns no data, so a reflex whose run is an argv names none, and declares no [yields] either.

What the body touches

[needs]
reads  = ["{place}"]                         # the value of an argument or a config key
writes = ["~/Downloads", "{to}"]             # under the home; or absolute, like "/tmp/out"
hosts  = ["*"]                               # the network, all of it; absent means none
runs   = ["open", "/usr/bin/plutil"]         # by name on PATH, or by absolute path

Each key is a list, and leaving the table out is the tightest declaration: the body's own directory and its private TMPDIR, nothing else. reads and writes name paths: ~/… under the home, absolute, or {name} for the value of an argument or a config key, so a folder the user names reaches the body and nothing beside it. A path names a file, or a folder with everything under it, whichever is on the disk when the body runs. A path in writes may be read too. A declared path must exist when the body runs. A body that makes a file declares its folder, and a path the machine lacks stops the run before the body starts: [needs] reads names ~/nowhere, which is not there; a folder in writes ends in the mkdir -p that makes it. hosts is all or nothing: ["*"] reaches every host, absent reaches none, and a name is refused at check. runs names programs, by name on PATH or by absolute path; each of them runs held to the same declaration, and what it asks the system to do happens outside it. The kernel holds the body to all of this: Security.

A {name} names an argument that carries a value, a word's or a typed one, or a config key; evoke check refuses one that names neither, or a flag. An entry over an optional argument left unstated is dropped. A value that is not a path, a word whose value is a name rather than a folder, refuses the run and points at the value's source: [needs] writes names {to}, whose value "desk" is not a path → evoke vocab places add desk "<meaning>" --value <path>.

A reach past the declaration ends the run with what was reached and the key, ~/secret.txt is not in [needs] reads, and a fix by where the declaration is written: this manifest's line for a local reflex; for a fetched one, evoke update --accept when upstream's declaration already allows it, else evoke remove. [needs] is contract: an overlay cannot touch it; upstream may narrow it at any tag, which is same; widening waits for the user's evoke update --accept, and is minor: Publishing.

Text rules

  • Every string is clean text: no control characters, except the line feed in description, and no bidi controls. evoke refuses the rest, so a manifest can never repaint a terminal.
  • Names of arguments, tags, config keys and vocabularies match [a-z][a-z0-9_]*. An argument name is also never a JavaScript reserved word, so a body can destructure it.
  • Option keys are one clean line each. none and unstated are reserved.
  • Unknown keys are reported by evoke check, never fatal: unknown lights: warmth is not a key this evoke reads. The schema at https://evoke.build/schemas/reflex.json is strict, so an editor flags a typo before evoke check does.

What is contract, what is wording

Contract: a user cannot override it, and changing it is a version bumpWording: a user may override it, and you may improve it at any tag
run or steps; platforms; [needs]; argument names and their sources; option keys; range; recent; config keys; yields; returns; what an argument takesdescription, not_for, tags, confirm; every ask; the meaning of each option; examples and tests

An argument may be renamed by declaring its former names, like was = ["state"]. Every user's overlay and call then follows. Publishing says how evoke check diffs one tag against the next.

Lint

At evoke check and evoke add, lint reports and never refuses. It flags a text or a list longer than its cap, a phrase that addresses a model, a step a plan would misread, and what a reader of the file would miss, like effect left out or fewer than three examples. Each finding names the key, and the text is yours to weigh. Diagnostics lists every line.

Next: Arguments.

Arguments

An argument is a value the body receives. Most are also a question the classifier answers about the input: each of those has an ask, and exactly one source for its values: the author, the user, or the input. An argument with takes has a fifth source, an earlier step's result, and no ask: nothing is asked.

[args.state]
ask = "What should the lights do?"          # the question a person would be asked; sent verbatim
options.on  = "Switch on."                   # the author's closed set
options.off = "Switch off."

[args.room]
ask   = "Which room of the house?"
vocab = "rooms"                              # the user's closed set: vocab/rooms.toml

[args.brightness]
ask      = "How bright, in percent?"
pick     = "number"                          # a span of the input, read by a built-in recognizer
range    = [1, 100]
optional = true

[args.clipboard]
ask  = "To the clipboard instead of a file?"
flag = true                                  # a yes/no switch

The five sources

SourceValues come fromThe body receivesAsked when missing
optionsThe author: key = "meaning"The keyA numbered choice
vocabThe user: vocab/<name>.tomlThe word's value if set, else the wordA numbered choice, plus [+] add one
pickThe input, as typednumber → the number; duration → whole seconds; email, url, quoted, code → the text; date → the day, 2026-05-05; time → 17:30; amount → { amount, currency }Typed freely, read by the recognizer; a recent field's values numbered before it
flagThe inputtrue, or nothingNever
takesAn earlier step of the same request, whose reflex returns that nameThat step's data, as it returned itNever: the request stops before anything runs

A value from an open set is a pick, a switch is a flag, options are for a closed set, and a value only the user knows is a vocabulary: rule 4 and rules 19 and 20.

ask is the question a person would be asked. The same line serves the classifier and the prompt. Each ask is one question, as a person would ask it: rule 17.

optional = true means an unstated argument is omitted and the body's own default applies. A required argument left unstated makes evoke ask. A flag is optional by nature and takes no optional.

Options

The author's closed set. The key is what the body receives and what a call shows, as in state="off". The meaning is what the classifier reads. Keys are one clean line each, never none or unstated. Examples that assert an option teach it: "kill the lights" = { state = "off" }. A key is a word a person knows at a glance, and its meaning answers the ask in one short line: rule 18.

Vocabularies

The user's closed set, by name. A reflex that names a vocabulary, vocab = "<name>", reads the words the user put in vocab/<name>.toml. A package never ships or writes one. An empty vocabulary makes the reflex inactive until a word is there, when the argument is required. An optional argument over an empty vocabulary is never asked and never stated, and the reflex stays active. A word's value is for the body only: a path, a URL, a device id. Reflexes share a list only by naming it alike, so each kind of thing has one published name: Vocabulary names.

A shipped manifest may not assert a vocabulary argument in its records, not even as unstated. The words are not yours to know. A user's own overlay may.

Picks

A pick reads a piece of the input, word for word. Nine recognizers exist:

pickRecognizesValuerange
numberA number in digits or in words up to the hundreds, unit words kept in the span: 30 percent, fifty percent, twenty-five, two hundred and forty; a leading minus is the number's: -5The numberyes
durationOne number and one unit, the number in digits or in words: 10 minutes, 2 hours, twenty five minutes; an hour, half an hour, a quarter of an hour, an hour and a half, two and a half hours. A length across units, 1 hour 30 minutes, is read from a sentence as a value said aloud, and at a prompt or in a call by name as it is typedWhole secondsyes
emailAn addressThe textno
urlA URLThe textno
quoted"…", “…” or ‘…’; a straight single quote is an apostropheThe text between the quotesno
dateA day: today, tomorrow, tonight, yesterday, the day after tomorrow; a weekday, alone or after next, this or last; a day of the month: on the 14th, friday the 14th; a month and a day, with a year or not: may fifth, March 6th 2017, the 22nd of march; in two weeks, three days from now; 2026-05-05; 27/03/2017 when one order alone is possible. Never a period, a month or a year alone, every monday or nowThe day, 2026-05-05, on your machine's calendar when the program runsno
timeA clock time with its half of the day: 5pm, 7 a.m., 5:30 pm, seven thirty am, six in the morning, 4 o'clock in the afternoon, noon, midnight; or a two-digit hour on the 24-hour clock: 13:00, 09:30. An hour word alone is a number: at three. 5:30 and 4 o'clock with no half read as nothing at all. Type 3pm, 15:00 or 05:30. in ten minutes is a durationThe time, 17:30no
amountA figure or number words with a currency: $30, €1,200, £19.99, 50 dollars, twelve hundred euros, 1000 USD. $ is the US dollar. A currency alone is nothing; pound after a count is a weight, pounds is moneyThe number and the currency's code, { amount, currency }no
codeAn identifier as typed, in capitals or small letters: a version 4.12.0, a ticket INC-311, a flight tp1043, a serial C02XK1ABJG5M; quoted or not. order 4821 is a number, 10mins a durationThe textno
  • The classifier chooses among the candidates found. It never invents one. A pick with no candidate reads as unstated. If it is required, evoke asks for it.
  • A text typed without quotes is read for a quoted argument where the classifier says the input states one: in set a timer for 10 minutes called tea, the label is tea. The text is a run of the input's words, copied, and the call confirms.
  • A value spelled out is read as it is typed, where the classifier says it is meant: an address, a URL, a code, a number, a day or a length of time said aloud, dana dot weiss at example dot org, five dot oh dot two, an hour and twenty minutes; a weekday misspelt, wendesday; a code typed with a space, hs 0409. The call shows the typed value. After a misspelt day or a spaced code it confirms, with your words on the line (Outcomes).
  • A pick's examples set its shape. The values your examples state for a code or a number argument tell evoke how a value said aloud is written: where every example reads like HS-0409, k d oh three one five becomes KD-0315, and the call confirms. Where the examples differ in case, the call confirms too. Where they leave the length open, or the words read two ways, evoke asks and offers what the words read as. A code argument with no example value is read only as it is typed. So give each pick argument examples that show its real forms (Saying things).
  • Text people type freely is never a quoted argument, and the body reads it from input: rule 21.
  • Quotes hide what they enclose. A typed span hides the bare numbers inside it. In timer for 3 minutes called "eggs", the duration is 3 minutes, the quoted text is eggs, and 3 alone is never a number. A date, a time, an amount or a code hides its figures the same way: in pay €1,200 on may fifth at 5pm no bare number is a candidate.
  • A span that a recognizer reads and no argument takes is left to the words around it: where they ask for another thing, or the call holds less than was typed, the call stops at confirm (Outcomes). A vocabulary word that the person typed is never such a span, even where a recognizer reads it too: tomorrow in a vocabulary of days is the word, not a date.
  • A date is resolved when the program runs, against your machine's clock in its own time zone, or against EVOKE_TODAY when it is set (Environment). Before that, everywhere evoke shows the value, it shows your words; in a plan file, tomorrow means the tomorrow of the run.
  • range = [min, max] compares the value, in seconds for a duration. It is allowed on number and duration only. Out of range, evoke asks, with the reason: 150 percent is outside 0–100.
  • In a confirm template, the placeholder shows the span. In an argv, it shows the value's text; an amount cannot stand in an argv.

A value recalled

[args.release]
ask    = "From which release?"
pick   = "code"
recent = "release"                           # what earlier results of this session hold under that field

A pick may name a field of [yields] with recent. When the words leave the argument unstated, evoke lists the values the bodies of this session returned under that field, newest first and five at most, each one the recognizer reads whole, and asks:

> list the checkout deploys
  deploys service="checkout"  0.95
checkout: 2 deploys today, the last 4.12.0 at 13:58
> roll back the last deploy
  From which release?  [1] 4.12.0  [2] 4.11.3  > 1
  rollback release="4.12.0" · destructive · weakest: route 0.95
    it cannot be undone, so it always waits for a yes
  Roll back 4.12.0?  [y]es [n]o [t]each > y

A number picks one; anything else is read as typed. A number pick names the values as hints instead, since a number typed is its own answer. Words that point at one of the values take it without a question: it, that order, this one, the last order, my latest order, the most recent order, with the argument's name or the field's as the noun. After a product is shown, buy it buys it; after two orders, pay that order pays the newest, and pay the last 2 orders, those two orders or both orders pay the two newest, one step each. evoke why names the words and the value's place: the newest of this session's results, named by "it". A number before the plural of the argument's name, 2 orders, is a count and never the value; where the call still has to ask, the prompt names those words. the order, them and a bare count point at nothing, and the ask lists the values as above. The session is one process: the REPL and the stdin filter remember every result, a one-shot call remembers none, and in the SDK an application hands the results it keeps. A sentence of several steps takes nothing from memory: its second step asks as it always did. The classifier is never asked about a recalled value; the words or you choose, and evoke why names what was offered. recent is contract, beside a pick only.

Flags

flag = true is a yes/no switch. evoke reads the classifier's answer as yes at or above one half. The body receives true, or nothing. A flag never appears in a confirm template, and it cannot be an argv element. A body that needs one is a file. In records, a flag is asserted as { clipboard = true }.

A whole result taken

[args.deploys]
takes = "deploys"                            # an earlier step's whole result, by the name its manifest returns

A takes argument is required, and stands alone: no ask, no optional, no range, no was. Its reflex runs only after a step that returns the name, in one sentence (Weaving): alone, when no step before it returns the name, or when two do, the request stops before anything runs and says which. The argument's own name is the author's; the plan and evoke why print the result's. The body receives the data as its source returned it, and checks its shape itself: reflex.d.ts types it unknown. A confirm cannot show a whole result, so a reflex that runs destructive takes none: it takes what it acts on as a field of [yields], which its confirm names. evoke run cannot give a takes argument, and no sentence states one.

Names and renames

An argument name matches [a-z][a-z0-9_]*. It is never a JavaScript reserved word, like for, class or default, so a body can destructure it. When one kind of value plays two roles, each role is an argument of its own, named by the role: rule 3. To rename an argument, declare the old name and keep it declared forever:

[args.power]
ask = "What should the lights do?"
was = ["state"]

Every user's overlay and every call written with the old name follows the rename at merge time. was is flat and cumulative. A retired name never returns as a live argument, and it never leaves the list. evoke check enforces both against the previous tag. update tells each user that their records followed.

In the confirm line and in an argv

confirm = "Set the {room} lights {state}?" names required arguments only, never one with takes. The confirm reads the call back: rule 23. In an argv run, a placeholder is a whole element, as in ["hue", "set", "{room}", "{state}"]. It names an options, vocab or pick argument; a date stands as 2026-05-05, a time as 17:30. An element whose optional argument is unstated is dropped. Literal braces, flags, an amount and a taken result need a file body.

Next: The body.

The body

The body is what run names. It is either a JavaScript file with a function as its default export, or a program with its arguments. evoke calls it once per decision, with the arguments filled in, a context, and a deadline.

A file body

run = "lights.mts"
import type { Reflex } from "./reflex.d.ts"     // Args, Config, Context, Result, Reflex — written by evoke check

export default (async ({ room, state, brightness = 30 }, { config, signal }) => {
  await hue(config, room, { on: state !== "off", bri: state === "dim" ? brightness : 100 }, signal)
  return `${room} lights ${state}`
}) satisfies Reflex
  • The file ends in .mts or .mjs and lives inside the reflex directory. It is a module, whatever a nearby package.json says. It runs on Node 24 or newer, which strips types on its own. There is no build step.
  • It is self-contained. Web APIs and node: builtins are there, and evoke installs no package for it. The body is one file, with anything else bundled into it: rule 44.
  • satisfies Reflex types the arguments from the manifest and checks the return. evoke check writes reflex.d.ts next to the body whenever the arguments change. Commit it.

What the body receives

(args: Args, context: { input: string; config: Config; signal: AbortSignal })
ValueIs
argsPer argument: an option's key; a vocabulary word's value, or the word; a pick's value, meaning the number, the seconds, or the text; true for a flag; for an argument with takes, the whole data of the earlier step that returned it, as it was returned. An optional argument left unstated is absent
inputThe person's own words, for a typed sentence. When a plan runs the reflex as a step, the step's words. Empty when evoke run calls it by name. Free text, a reminder's message, is read from here: rule 21
configEach [config] key as a string, secrets resolved from the environment for this run only
signalAborted at the deadline, on Ctrl-C or SIGTERM, when an app cancels the run, and when evoke ends. The body then stops: rule 44

What it returns

A string: the text a person reads. Or { text, data? }, where data is anything an app might use: a pid, a time, a path. A field of data that a later step of one request may take is declared under [yields] in the manifest, and the whole of data, under a name, with returns: The manifest.

Throwing is failure: the message prints on one line, and the exit code is 1. Anything else returned is a failure too. A thrown error's frames are kept for its author: after a typed sentence, evoke why shows them under its decision, and --json carries them as frames.

The text says what happened, with the value that lets a person check it: rule 35. A failure says what went wrong, in the body's own words: rule 44.

The run

  • Environment. Scrubbed: PATH, HOME, TMPDIR, LANG and TERM, nothing else. TMPDIR is a private folder, made for the run and removed after it. Secrets reach the body through config, never the environment.
  • Reach. The body touches what [needs] declares, and nothing else: The manifest. Its own directory and TMPDIR are always there. A read past the declaration throws in the body, and the run ends with what was reached and the key, ~/secret.txt is not in [needs] reads, and the fix the manifest page gives by where the declaration is written. A word's value or a plain setting that begins with ~/ reaches the body as a path under the home, in config, in an argv and in EVOKE_CONFIG_<KEY>, so the value, the argv and the declaration name one path.
  • Deadline. 30 seconds per decision, shared with the classifier's answer. At the deadline signal aborts. A body still running a second later fails. What must outlive the run, like a timer or caffeinate, detaches: rule 44.
  • Output. The result is what the function returns. The body's own stdout and console go to stderr, so a stray console.log never corrupts a result.
  • Process. The runtime starts once the decision is made, in the body's directory, held by the kernel. Its life is bounded by evoke's. On Ctrl-C, and for a body that will not end, the process group is ended: SIGTERM, a short grace, then SIGKILL.
  • Platform. On a system platforms leaves out, the reflex is inactive, and add and show say so: runs on macOS only. When the body runs on one system alone, name it in platforms: rule 43.

An argv body

run = ["networksetup", "-setairportpower", "en0", "{state}"]

A program and its arguments, run directly, never through a shell. The first element is a literal: the program. It is a name found on PATH, or an absolute path. It is never a path relative to wherever evoke runs. A placeholder is a whole element. It names an options, vocab or pick argument, and is replaced by the option key, the word's value or the word, or the pick's value: the number, the seconds, or the text. A date stands as 2026-05-05, a time as 17:30. An element whose optional argument is unstated is dropped. A value that would start with - is refused. Config arrives as EVOKE_CONFIG_<KEY>, and the input as EVOKE_INPUT. Stdout is the result. A non-zero exit is failure. No runtime is needed. The program is the body, so it runs without being named in runs, held to what [needs] declares.

Flags, an amount, a taken result and literal braces cannot be expressed in an argv. Write a file instead.

The generated types

// reflex.d.ts — generated by evoke check; do not edit.
export interface Args {
  /** How long? */
  duration: number
  /** What is the timer for? */
  label?: string
}
export interface Config {}
export interface Context { input: string; config: Config; signal: AbortSignal }
export type Result = string | { text: string; data?: unknown }
export type Reflex = (args: Args, context: Context) => Result | Promise<Result>

Option keys become a union: "on" | "off" | "dim". A word, a quoted text, an address, a URL and a code are string. A number and a duration are number. A date is a string, 2026-05-05, resolved against the day the body runs; a time a string, 17:30; an amount { amount: number; currency: string }. A flag is true. A taken result is unknown, since evoke checks no shape. An optional argument is marked ?. Each member carries its ask or about, so an editor's hover shows the question. The file imports nothing.

Testing a body

A body is a function. Import it and call it. The collection's own tests do exactly that, under node --test, with a context built by hand: { input: "", config: { file }, signal: new AbortController().signal }. Nothing of evoke is needed to unit-test a reflex.

Next: Playbooks.

Playbooks

A playbook is a reflex whose body is a plan: steps, one sentence per step, and no run. A person reaches it with a short sentence that states the situation they face: «checkout is failing in eu-west». That sentence is routed by your description, not_for and examples, like any request. Each step is then decided over what that person has installed, as a typed sentence would be, and the whole plan prints before anything runs. The playbook's summary and examples state the situation, and its not_for names the steps typed alone: rules 5 to 7.

# outage/reflex.toml
reflex = 1

description = """
Handle a service that is down or failing.
Checks its errors, deploys and logs, finds the release behind the failure, rolls it back, tells the incident channel and updates the status page; the rollback asks again before it runs."""
not_for = [
  "a service's error rate on its own",
  "listing a service's deploys on its own",
  "pulling a service's logs on its own",
  "rolling back one release on its own",
  "posting to a channel or updating the status page on its own",
  "writing up an incident's postmortem",
  "asking whether a service is healthy",
  "a slow service that still answers",
]
tags = ["outage"]
effect = "destructive"
confirm = "Run the outage plan for {service}?"
steps = [
  "check {service}'s errors[ in {region}]",
  "list {service}'s deploys[ in {region}]",
  "search {service}'s logs[ in {region}]",
  "find the release behind it",
  "roll {service} back from that release[ in {region}]",
  "post to the incident channel",
  "update the status page for {service}",
]

[args.service]
ask   = "Which service is down?"
vocab = "services"

[args.region]
ask      = "In which region?"
vocab    = "regions"
optional = true

[examples]
"payments is down in us-east"    = {}
"search is broken"               = {}
"we have an outage on checkout"  = {}
"checkout is on fire in eu-west" = {}
"payments stopped working"       = {}

[tests]
"checkout is failing in eu-west"               = {}
"we lost search in us-east"                    = {}
"how bad are the errors on payments right now" = false
"downgrade search from 6.0.1"                  = false
"ping #ops that checkout recovered"            = false
"flip the status page to resolved for search"  = false
"prepare a retro on the checkout outage"       = false
"is the search service doing fine"             = false
"checkout feels sluggish this morning"         = false
$ evoke "checkout is failing in eu-west"
  1  errors service="checkout" region="eu-west"  0.95 · outage 1
  2  deploys service="checkout" region="eu-west"  0.95 · outage 2
  3  logs service="checkout" region="eu-west"  0.95 · outage 3
  4  suspect  0.95 · takes errors from 1, deploys from 2, logs from 3 · outage 4
  5  rollback service="checkout" region="eu-west" · takes release from 4 · outage 5
  6  post channel="#incident"  0.95 · outage 6
  7  status component="checkout"  0.95 · outage 7 · service as component
  outage service="checkout" region="eu-west" · destructive · weakest: route 0.95
  Run the outage plan for checkout?  [y]es [n]o [t]each > y

Every line of the plan ends with the playbook and the step that wrote it. Then comes the sentence's own line, the decision that picked the playbook, and one question over the whole plan. A step that takes a result waits for the steps it takes from, as in any sentence of several steps (Weaving).

A plan of sentences

A playbook is fetched, locked, tested, overlaid and shown like any reflex. What differs is the body: steps instead of run, and none of a body's keys, [needs], [config], [yields], returns, platforms or an argument's takes. evoke check refuses each of them. A plan holds at most 24 steps. It lists the situation's steps in the order they happen: rule 8. Its effect is your claim about the whole; add says when a step reaches a tighter one. The claim is the worst effect its steps reach: rule 10.

evoke new --playbook <name> writes one to start from: <name>/reflex.toml alone, a plan of three steps from the template, which checks clean.

Slots

A slot, {service}, is an argument of [args]: a vocabulary word, an option or a pick, never a flag. It is filled from the sentence, or asked before the plan prints, and written into the step as the person typed it, an option as its key. A word beyond the slots fills nothing. A value is never the playbook's own: a word the person lacks joins their vocabulary with [+] add one.

$ evoke "we have an outage"
  1  outage · asks service
  Which service is down?  [1] checkout  [2] payments  [3] search  [+] add one  > 2
  1  errors service="payments"  0.95 · outage 1
  …

Words in square brackets go only with the slot inside them: [ in {region}] is written when a region was stated and dropped when it was not. An optional argument stands only in a bracket, and a required one never does: evoke check refuses a slot in the wrong place. It also refuses a slot that is a flag or no argument at all, and an argument that no step names. A slot goes in every step whose reflex reads its value: rules 25 and 26.

Every step is decided when the plan is made

A step is decided by the classifier over what the person has installed, from its words alone, as a typed sentence would be. So each step is one action, worded as a person would type it: rules 24 to 29. evoke check refuses a step that reads as a call with a value, errors service={service}. Lint reports a step that holds a connective or a comma, states a channel or an address, or opens with check that. It also reports a step whose words are all the summary's or an example's. Diagnostics lists each line.

A step may reach another playbook, once: a plan inside a plan inside a plan is refused. A step that reaches the playbook it stands in is refused too. A step no reflex matches refuses the whole plan before anything runs:

  7  "update the status page for checkout" · no reflex · outage 7
[2]

evoke add shows what each step reaches on the set it joins, and evoke test fails a step that reaches nothing (Examples and tests).

One yes over the whole plan

Nothing runs before the whole plan is printed and one yes is given over it: your confirm when the plan is your playbook's alone, Run the plan as it stands? when the sentence holds more. A step's own confirm, a destructive one's above all, is asked again at its turn, every time. [t]each at the whole-plan question teaches the route to your playbook; at a step's turn it teaches the step's reflex from the filled sentence.

A result picks among the steps you listed

A step may run only under a value an earlier step yields. Write it as a table, say the sentence and when one field with the value it runs under, as text, and list the alternatives together right after the step whose result picks among them:

steps = [
  "drain connections off the primary",
  "fail over to 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" } },
]

A result never writes a step: it picks among the steps you listed, and the whole list prints before anything runs. The field is one that step's reflex declares under [yields] as one value, never a list. The plan refuses before anything runs when the step reaches a reflex that yields no such field, or the value is one the field cannot hold. evoke check refuses a when on step 1, where no step comes before it, and a when on two fields.

The step's line ends then 4 on "yes", 5 on "no", and each alternative's if 3 yields landing "yes". At that step's turn the result is compared with each value as text. The step it picks runs, the others are skipped clean and say so at the end, and a value no step lists picks nothing, the plan going on. A step without when runs either way. A step that may not run is one step: a later step cannot take its result, and its words may not reach another playbook. A check whose result picks is a reflex of its own that yields one field: rule 9.

$ evoke "drain the primary"
  1  drain  0.95 · runbook 1
  2  failover · destructive · weakest: route 0.95 · runbook 2
       it cannot be undone, so it always waits for a yes
  3  verify  0.95 · runbook 3 · then 4 on "yes", 5 on "no"
  4  tell channel="#ops"  0.95 · runbook 4 · if 3 yields landing "yes"
  5  failback · destructive · weakest: route 0.95 · runbook 5 · if 3 yields landing "no"
       it cannot be undone, so it always waits for a yes
  runbook · destructive · weakest: route 0.95
    it cannot be undone, so it always waits for a yes
  Run the failover runbook?  [y]es [n]o [t]each > y
primary drained
  2  failover · destructive · weakest: route 0.95
       it cannot be undone, so it always waits for a yes
  Promote the replica now?  [y]es [n]o [t]each > y
replica promoted
writes landing on the new primary
told #ops
  5  failback · destructive · weakest: route 0.95 · runbook 5 · if 3 yields landing "no" · skipped · not chosen: step 3 yielded landing "yes"
       it cannot be undone, so it always waits for a yes

What the person adds to the sentence

A part of the sentence that repeats a step of the plan folds into that step, and the plan says so: folded "show me the error rate" into 1. A part that states the situation a second time, «payments keep timing out, customers can't pay», folds into the plan the first part opened, so the plan holds each step once. Its folded line names that plan's first step: folded "customers can't pay" into 1. A part that says what not to do beside a plan is refused, and so is a part that opens with a condition, if, unless or in case. evoke judges no condition, so the line it prints says to ask for the check first, then what to do.

What is contract

steps is contract: an overlay may reword the description, the confirm and the asks, never a step. A step added, removed, reworded or reordered is a major version, and evoke check says so: steps the steps moved. A slot added or renamed rewords a step, so it is major too, and so is a when added, moved or changed.

add, test and show

add decides every step of a newcomer playbook over the set it joins, runs nothing, and prints what each reaches under its row; a step that reaches nothing installs and is reported, since the plan refuses when it is made. Two lines more may follow. claims write; its steps reach destructive says a step reaches a tighter effect than the playbook claims. step 3 reaches logs, which lacks the tag outage says a request narrowed with --tag outage would leave that step without its reflex. Every reflex the steps reach carries the playbook's tag: rule 40. add also prints also fits for the examples of the reflexes the steps reach, since the plan does hold their action. Typed alone, such a sentence still reaches its own reflex: Exceptions.

test decides the playbook's records as any reflex's, then its steps, each filled from the first record whose reading fills every slot it holds; a step no record fills is untested. Both say when the step before a branch reaches a reflex that yields no field the branch waits on, and test reports a claim the steps pass too. show prints the steps after confirm, numbered, a step that may not run ending in its when.

$ evoke test outage
  outage  16 passed · 7 steps route

In the SDK

playbook(manifest) hands a playbook as code, steps in its manifest and no body; project.steps(input, { ask }) answers a slot the sentence lacks before the plan is made (Decisions). A playbook is never called by name: run, handle and evoke run <name> refuse it with say it in a sentence.

Next: Examples and tests.

Examples and tests

Two tables, one shape. [examples] are sent to the classifier: they teach. [tests] are held out, never sent: they check. One line is documentation, tuning and test at once:

"kill the lights" = { state = "off" }

A user's overlay adds lines of the same shape.

The shape

[examples]
"turn on the kitchen lights"                = { state = "on" }          # asserts the route and one argument
"lock the screen"                           = {}                        # asserts the route alone
'timer for 3 minutes called "eggs"'         = { duration = "3 minutes", label = "eggs" }
"grab part of the screen to the clipboard"  = { area = "selection", clipboard = true }
"take a screenshot"                         = { area = false }          # asserts that area is unstated

[tests]
"make it darker in here" = { state = "dim" }
"light a candle"         = false                                        # never this reflex
AssertionMeans
{}The input routes here; nothing said about arguments
{ arg = "key" }An option: its key
{ arg = "span" }A pick: the exact span, which must occur in the utterance. For quoted, the text between the quotes
{ arg = false }The argument is unstated
{ flag = true }The flag is raised
falseNever this reflex. Such an example teaches the no side, alongside not_for

An argument a record does not name is not asserted. A shipped manifest may not assert a vocab argument, because the words are the user's. A user's overlay may.

Identity

Records are keyed by the utterance as written, and the classifier reads that spelling. Two records can still be the same utterance under identity. Identity means NFC, lower-cased, whitespace collapsed and trimmed, and a trailing . ! ? or … dropped. Such records are one record, and duplicates in one file are refused. A user's line naming a shipped utterance replaces it. So the highest layer decides each utterance's table and value.

Which records to write

The rules say which records to write, and why:

  • At least three examples, as people type the request: rule 14.
  • Each near neighbour, a sentence that sounds like the reflex's request and is not, as a false test in words no example uses: rule 15.
  • For each option or pick argument, one record that leaves it out, { area = false }: rule 15.
  • No record that opens with a negation or a condition: rule 15.
  • For a playbook, records that state the situation it handles, never one of its steps: rule 6.

evoke test

$ evoke test
  lights  3 passed · 1 failed
    "make it darker in here"  state: expected "dim", read "off"
  timer   8 passed
    "ping me in 2 hours"  passes, but would wait for a yes: the call holds all of it at 0.20, and a call runs at 0.30 or more
  1 of 12 cases failed  →  evoke test
[1]

Every example and every test of every active reflex is decided over the whole installed set. The classifier sees the other reflexes too, so a near neighbour installed next to yours is part of the test. It never goes through the cache, and it is never logged. A case passes on its route and on each asserted argument:

  • A false case passes on abstain, or on another reflex.
  • An ask reads its missing arguments as unstated.
  • A confirm is judged by its call, not by whether it would have run.
  • An argument the record does not name is not compared.

A case that passed at the last run and fails now is decided twice more. Two of three failing marks it a regression. test exits 1 when a case failed. It never blocks an install.

A case that passes while its call would wait for a yes, because the call holds less than the case's own words, is named under its reflex, with how far the call holds them. It counts among the passed and fails nothing. The cure is the summary: write it so that the case reads as what the reflex does (rule 11).

A playbook's steps are tested after its records (Playbooks): each filled from the first record whose reading fills every required slot it holds, decided over the whole set, and passed when it reaches a reflex other than its own playbook. Its line counts them, outage 14 passed · 7 steps route, and a step that reaches nothing, or its own plan, fails and prints under it. These print under it too, and never fail:

  • A step no record fills, marked untested.
  • The step before a branch, when its reflex yields no field the branch waits on.
  • A step that reaches a tighter effect than the playbook claims.

Lint

Records count against lint: at most 40 per table, and 200 characters per utterance. Lint also reports fewer than three examples that are not false, an option or pick argument that no record leaves out, and a quoted argument that no example shows in quotes. Lint reports at add and check.

Next: The rules.

Rules for reflexes, playbooks and collections

The keys are defined on The manifest, Arguments and Playbooks. This page says how to write them so that what people type reaches the right reflex, is asked only for what it left out, and never runs a step nobody asked for.

Everything the classifier knows about your reflex is your manifest, read word for word with every sentence a person types. It chooses one reflex or none: the route. It reads each argument's value, or answers unstated. The gate then turns those answers and the effect into an outcome: a call runs unasked only when every value it holds clears the floor its effect sets and the call holds all that was typed, and otherwise evoke asks for the value, or for a yes. When a reflex is added, the classifier also answers fits for every example installed: does the new reflex do what that sentence asks? The person reads the call or the plan, and gives the yes. Neither the classifier nor the person knows what you meant and did not write. So the careful thinking is yours, done once, in the words of the file.

Four words come up throughout. A lookup is a reflex whose effect is read. A change is one whose effect is write or destructive. A record is one line of [examples] or [tests]. A yield is one field of a body's result that a later step may take, declared under [yields] with the kind that reads it; a whole result travels instead by the name returns gives it and takes asks for.

Each rule gives its reason. Where the tool holds a rule for you, a word in italics after it says what: (check) refuses the manifest, (lint) reports at check and add and never refuses, (add) reports when a reflex is installed, (plan) refuses the sentence before anything runs. A rule with no such word is yours to keep. Each section ends with a line that keeps its rules, one that breaks them, and what the terminal prints.

Everything you write has a reader:

What you writeRead by
The reflex's name, the last segment of its directory or repositoryThe classifier, as one of the route's choices. The person, first on every call line
description, not_for, [examples]The classifier, with every sentence. The person, in evoke show and in the file
An askThe classifier, as the argument's question. The person, at the prompt
An option's keyThe classifier, as the choice's key. The person, at the prompt and on the call line. The body
An option's meaning, a vocabulary word's meaningThe classifier, as an answer to the ask
A vocabulary word's valueThe body, and [needs] where it names the argument. Never sent
stepsThe classifier, each step as a typed sentence over the set it runs on. The person, as the plan's lines
effectThe gate, as the floor a call must clear before it runs unasked. The person, when a call confirms
confirmThe person, before the action
[needs], platformsThe kernel, which holds the body to them. The person, at add
[tests]evoke test and evoke calibrate. Never sent
tags--tag, which narrows a request. Never sent
The result lineThe person, after the run

1. One reflex, one action

  1. One reflex is one action, with one effect and one confirm, since the effect sets how sure evoke must be before every call of the reflex: a gentle action bundled with a harsh one confirms like the harsh one. The states of one action are the options of one argument, like screenshot's area, so a sentence that names no state still reaches the reflex. Split the states into one reflex each, and that sentence reaches one of them by a narrow margin, or none.
  2. When a plan runs a lookup and then the change it serves, make them two reflexes: device finds the laptop and lock locks it. A lookup runs without asking on weaker evidence than a change, and one lookup can feed several changes. Where people also type the change alone, give the change a word of the sentence to read, «Which colleague's laptop?», since a reflex that only takes a whole result is refused when nothing before it returns one. (plan)
  3. Give a reflex only the arguments its body uses, since each argument is one more answer and a call's confidence is the weakest of them. An argument the body cannot run without is required, and so is a value the confirm must read back, since a confirm may name only required values. Make an argument optional only where running without it is what the person expects, since an optional value the classifier misses is dropped, never asked. When a value of one kind plays two roles, a day of departure and a day of return, give each role its own argument, named by the role, since one question cannot read two answers. When people state one value in two forms, a new time or a shift, «to 4pm» or «15 minutes later», make the form the confirm can show the one required argument, since no key makes one of two required; the other form then stops the call at confirm (rule 4).
  4. A value from an open set is a pick, a switch is a flag, and options are for a closed set that covers every value people mean, since a value with no key is asked. When people type a kind of value beside the action and no argument reads it, the call holds less than they typed, and stops at confirm where that shows: so give a value an argument when the body can use it, and always when the body acts on a quantity.
# kept: three states of one action, one effect, one confirm
[args.area]
ask               = "The whole screen, a window, or a selection?"
options.screen    = "The whole screen."
options.window    = "One window, chosen by clicking it."
options.selection = "A rectangle you drag out."
optional          = true

A screen, a window and a selection reflex each read «fire off a quick screencap», cost three descriptions on every request, and share the route between them, so the call stops at confirm or nothing runs.

2. When to write a playbook

Playbooks has the keys. A playbook is a reflex whose body is a plan: steps, one sentence each, in place of run. A situation is what a person faces, typed in one sentence: a problem, «checkout is failing in eu-west», or a routine, «close the month».

  1. Write a playbook for a situation people type in one short sentence and your team handles with the same steps each time, since a sentence that states the situation reaches the playbook, and one that asks for the steps reaches their reflexes instead: such a request seldom opens the playbook, and is refused whole when a step takes what the plan's steps return. Steps that differ each time are the person's own sentence, not a playbook.
  2. Its summary, its examples and its tests state the situation, never a step's action, since a sentence that names a step reaches that step's reflex and the playbook is never reached. Its test is the short sentence a person types when the situation happens, in words no example holds.
  3. Its not_for names the steps people type alone, in their words, «draining the primary on its own», and the situations beside it that it does not handle, so one step typed alone reaches its own reflex.
  4. A plan holds one situation's steps in the order they happen, the lookups a change rests on first, since a person reads the whole plan before one yes; keep it to the steps the situation needs, since 24 is a cap and not a target. Every step keeps its own gate at its turn, a destructive one asking again, so a plan may be long: each step still asks what it must.
  5. When what happens next depends on what a step found, make that check a reflex of its own that yields one field, and list the alternatives right after it, one per value, each under when. A step that runs either way has no when. A step may reach another playbook once; a playbook reached from that one refuses the plan. (plan) No step holds a condition, a loop or a computed value, since evoke judges no condition and computes no value of yours.
  6. Its effect is the worst its steps reach, since add prints a claim its steps pass. (add)
# kept: the situation, no step named
description = """
Handle a service that is down or failing.
Checks its errors, deploys and logs, finds the release behind the failure, rolls it back, tells the incident channel and updates the status page; the rollback asks again before it runs."""
not_for = ["rolling back one release on its own", "a slow service that still answers"]

An example «check checkout's errors» on the playbook reaches errors, never the playbook; a step «check whether errors dropped, then roll back» is reported: step 4 holds "then"; one step is one action.

3. Words that reach the right reflex

The manifest has the keys.

  1. The summary, the first line of description, says the action and what it acts on, in the words people type, 100 characters at most, since a stranger reads it first. (lint) It says the action with what a person gives it, Look up a new hire by name, with their role and start date, never by what comes back alone, Look up a new hire's role and start date, since a call is read back against these words, and look up maria must read as what the reflex does. evoke test names an example whose right call waits for this. The second line draws the boundary: what the reflex covers, what it touches, and what cannot be undone, since a reader takes the effect from those words.
  2. Describe the action and never address the classifier, since one manifest serves every engine and every reader. (lint)
  3. not_for names the near neighbours: what a person might type that sounds like your reflex and is not. Write each line in the manifest of the reflex that must not take the phrase, since a line guards only the reflex that carries it. Add a line for a phrase you have seen taken, or a neighbour's example your reflex would take, since a line also turns away your reflex's own sentences near it. Never a generic phrase: every line is read with every request, and a generic one holds nothing. (lint) reports more than eight lines.
  4. Write at least three examples, as people type the request, since every example is read with every sentence and costs on every request. Take them from sentences people typed; varying the verb for its own sake buys nothing. Each asserts the options and flags its words state, which evoke test checks, and a pick as the exact span the sentence holds. (check) refuses a span the sentence does not hold; (lint) reports fewer than three examples.
  5. Write the near neighbours as false tests, in words your examples do not use, since a test proves a boundary only where the classifier never saw its words. Cover each option and each pick. For every option or pick argument, write one record that leaves the value out and asserts the argument unstated, { label = false }, since a required one is then asked. (lint) No record opens with a negation or a condition: typed, a negated part is left out and a conditional one refuses the request.
  6. Guard a change with a required value and an honest effect, never with wording alone, since only an ask or a confirm stops a wrong route. A question about the change that states its value passes the ask and the confirm alike, so a collection with a change also holds the lookup that answers such questions (rule 39).
# kept: the action, then the boundary; the near neighbours as false tests in new words
description = """
Set a timer that rings after a duration.
From seconds to hours, named in quotes if you like, "like this"; it rings with a notification and a sound."""
not_for = ["keeping the laptop awake for a while", "an alarm or a reminder at a time of day", "asking what time it is"]

[examples]
"set a timer for 12 minutes" = { duration = "12 minutes" }

[tests]
"give me a two hour countdown" = { duration = "two hour", label = false }
"ring at 6:30 am"              = false

not_for = ["anything unrelated"] holds nothing and is read with every request; "set a timer" = { duration = "12 minutes" } is refused: asserts duration = "12 minutes", which is not in the utterance.

4. Words that read each value

Arguments has the keys.

  1. Each ask is one question, as a person would ask it, since the classifier reads it word for word and the person sees it at the prompt. Never ask whether a value was given: every question already offers unstated, and none of these for a word the list lacks, which evoke then asks for by the person's own word. Keep the question short, since a longer one lost a word the list holds.
  2. An option's key is a word a person knows at a glance, since the prompt and the call line print keys alone: [1] restart [2] shutdown. Its meaning answers the ask in one short line, since every request carries every meaning. (lint) reports a meaning that repeats the ask.
  3. A value only the user knows, a folder, a service, a person, is a vocabulary, named by its published name below, since reflexes share a list only by naming it alike. Your records assert nothing about a vocabulary word, since the words are the user's; a user's overlay, the wording file kept over a shipped manifest, may.
  4. A value people type is a pick of its kind, number, duration, email, url, quoted, date, time, amount or code. A recognizer reads the forms its grammar knows and never invents one: a code as it is typed, TP1043 or tp1043; a quoted value between double quotes. A form it does not read is asked for. A range bounds a number or a duration, so a value outside it asks with the reason.
  5. quoted is for text people quote. Typed without quotes, a quoted value is read from the person's own words where they answer the ask, set a timer for 10 minutes called tea, and the call waits for a yes; where no words answer it, an optional one is dropped and a required one is asked. So text people type freely as the whole of the sentence is never a quoted argument: the body reads it from input. For a typed sentence, input holds the person's own words; when a plan runs the step, it holds the step's words. A message a playbook sends is therefore written into the step. (lint) reports a quoted argument that no example shows in quotes.
  6. Where people point at a value an earlier command found, recent on the pick names the field a lookup yields, so the ask lists the values this session found and the person chooses.
  7. The confirm reads the call back, its action and each required value, Roll back {release}?, and nothing else, since when a call asks it is the last line before the action and must say what this call will do. Never Are you sure?. (check) refuses a confirm naming an optional argument, a flag or a taken result; (lint) reports one that names no required argument, or asks Are you sure.
# kept: one question, a pick, a range
[args.level]
ask   = "How loud, in percent?"
pick  = "number"
range = [0, 100]

ask = "Did they say how loud, and in percent or not?" is two questions, one of them already answered; confirm = "Set the volume to {level}, {muted}?" is refused: confirm names {muted}, a flag.

5. How to word a step

A step is one sentence of a playbook's plan. A slot, {service}, is an argument written into a step.

  1. One step is one action, written as a person would type it, never as a call, check {service}'s errors and not errors service={service}, since each step is decided over whatever is installed, as a typed sentence is. A step may hold a verb that is also a reflex's name; only the call form is refused. (check) refuses the call form; (lint) reports a connective.
  2. Write the slot into every step whose reflex reads its value, required or optional, list {service}'s deploys. A vocabulary word the person typed also reaches a step written without its slot, list its deploys, since the plan carries a stated vocabulary word into a step that lacks it; a pick or an option is never carried, so its slot is always written. A step without the slot must not hold a word of that vocabulary, search its logs beside a service named search: the plan leaves such a step to read its own value, it reads none, and asks.
  3. Put an optional slot in a bracket with the words that go only with it, [ in {region}], and leave a required slot bare, since the bracket drops whole when the slot is unstated. (check)
  4. Describe a channel, «post to the incident channel», since the step then reaches whichever word of the person's channels vocabulary means that. Never write an address into a step, since a value is never the playbook's own; make the recipient a slot the sentence fills. A required slot the sentence leaves out keeps the plan shut until it is answered, and in a longer request a part that takes what the plan's steps return then has no source and the whole request is refused. (lint) reports a channel or an address written into a step, and calls both a slot: describe the channel, and make the address a slot.
  5. A step that takes a value an earlier step found refers to that step with it, them or its noun, wipe it, roll {service} back from that release, since a found value binds only through a reference; never they, which evoke does not read as one. Word a check check whether, never check that, since that before a noun names an earlier step. (lint)
  6. Word each step apart from the situation, its summary and its examples, since a step near the playbook's own sentence reaches no reflex, or its own plan, and the plan refuses. (plan) (lint) reports a step whose words are all the summary's or an example's.
# kept: one action each, the slot where it goes
steps = [
  "check {service}'s errors[ in {region}]",
  "list {service}'s deploys[ in {region}]",
  "roll {service} back from that release[ in {region}]",
  "post to the incident channel",
]

"errors service={service}" is refused: step 1 reads as a call: errors service={service}; a step is a sentence. "post to #incident" is reported: step 4 states "#incident"; a word another team would change is a slot, and the fix is to describe the channel.

6. How results connect

  1. Declare under [yields] every field a later step may take, by its kind, named by the noun a person would type for it, since a step takes only a declared field, and that release takes the field of that name from the step it refers to. A found value fills only a pick of the same kind, never an option or a vocabulary word. A lookup yields the value its change reads, in the change's kind and in its absolute form, a day as 2026-05-06 and a time as 09:30, two digits each on the 24-hour clock, since a step and recent take a yielded value only in that form, and 9:30 is nothing.
  2. A lookup that finds several things yields the one its change acts on, under that thing's noun, job = "number", whenever a plan's step takes it. The list, jobs = { each = { id = "number" } }, may stand beside it under another noun, for recent and for them, which runs the next step once per item. Over a list, that job stops the whole plan before anything runs, step 2 takes one job from step 1, which finds several — which one?, since nothing is picked for you.
  3. Give a whole result one noun across the collection, and one shape under it, since a reflex that takes it binds by the name alone and checks the shape itself. A reflex never returns a name it takes, and a destructive reflex takes a field its confirm can show, never a whole result. (check)
# kept: the field the next step takes, by kind and noun
[yields]
release = "code"

A deploys reflex with no [yields] gives «roll back that release» nothing to refer to, so the release is asked before anything runs: the plan's line ends · asks release.

7. What a person reads

  1. Always write effect, since left out it means destructive, and a reader sees that only where the description says the change cannot be undone. (lint) read observes. write changes what a later call can put back as it was, the volume, a light, a message to your own team. destructive ends or spends what no call brings back: a wiped disk, a purchase, a message to someone outside. A destructive call asks before it runs, every time; a read or a write runs without a question when the classifier is sure of every answer. A change that also sends a notice takes the notice's effect, judged by the widest audience the call's values can name, since a notice is never taken back. Claim the effect the action has, never a worse one to be safe, since a question before every harmless call teaches the yes a destructive one needs.
  2. Name a reflex by its directory, which add installs it as: the word people use for its action or its object, noun or verb alike, short, unique in the project it joins, since the name opens every call and plan line and the classifier reads it too. Name a playbook by its situation.
  3. The result line says what happened in one lowercase line, with the value that lets a person check it, volume 40%, locked, never done. A lookup that found several things counts them first, 3 jobs queued, then one line each.
  4. Keep the keys in the order evoke show prints them, after reflex = 1: description, not_for, tags, effect, confirm, steps or run, platforms, returns, then the tables, [needs], [config], each [args.<name>] with its ask first and its source next, the asked arguments before the taken, [yields], [examples], [tests]. In TOML a plain key written below a table header belongs to that table, so the plain keys come first; and a stranger then reads the file as the tool shows it: what it does and will not take, what it changes and asks before acting, what it touches, what it will ask, what it was tested on.
# kept: the effect written, the keys in show's order
reflex = 1

description = """
Roll a service back from a release to the one before it.
Everywhere or in one region; live traffic moves to the older release, and only a new deploy brings the newer one back."""
effect  = "destructive"
confirm = "Roll back {release}?"
run     = "rollback.mts"

With effect left out the tool still treats the rollback as destructive, and a reader who sees no word of undo takes it for a write; a confirm written below [args.x] belongs to that argument, and check says confirm is required.

8. How to design a collection

A collection is the reflexes and playbooks you publish together: one repository, versioned by one tag.

  1. Write the collection's shape before any wording: each reflex's name, effect, arguments with their sources, and what it returns and takes, since each reflex's words depend on its neighbours. The shape is the contract, what a user cannot override: the run or the steps, the arguments and their sources, the option keys, what a reflex yields, returns and takes. A reflex's words move freely under one tag; a playbook's steps are contract too.
  2. When the collection acts on things the user names, a service or a printer, its topic is one vocabulary every reflex that acts on them names, services across an outage collection, since a word typed once reaches the steps that lack it only through that name. Things found by a pick, a meeting by its time or a job by its number, are no vocabulary: coin none just to have a topic. A vocabulary that exactly one installed reflex requires, a playbook among them, is that reflex's own while it stays the only one, and a typed word then reaches no other step through it.
  3. Add a reflex only for an action the collection lacks, since every installed reflex is read with every request and two that cover one action take each other's sentences and stop each other at confirm. Where people ask about what a change changes, hold the lookup that answers, so «what does the status page say?» has a home and never runs status.
  4. Every reflex a playbook's steps reach carries the tag the playbook carries, one tag per domain shared by the reflexes that serve it, since a request narrowed with --tag starves a step whose reflex lacks it and the plan refuses. (plan) (add) reports a step that reaches a reflex without the tag.
  5. Install the collection beside the reflexes it will live with, and read every steals and also fits line: each names a sentence two reflexes both claim. Fix a theft in the not_for of the reflex that steals. (add) Two reflexes of one name are installed under different names, with add --as.
  6. The README holds one table, a row per reflex with what it does, its effect, each argument's source, what it yields, returns and takes, and what the user sets, since a stranger chooses a collection there and it is the collection's shape.
$ evoke add ./outage                               # kept: the playbook installed last, every step reaching
+ outage  ./outage  destructive  a plan of 7 steps
  1  check {service}'s errors[ in {region}] → errors · read
  …

A newcomer restart for a service prints restart: steals "restart this computer" from power, a sentence two reflexes both claim. Under --tag outage, a step whose reflex lacks the tag prints "list checkout's deploys" · no reflex, and the plan refuses.

9. What a body must do

  1. Declare in [needs] the tightest reach that runs, since the kernel holds the body to it and a person consents to it at add. What differs per machine is a [config] setting, a secret only as the name of a variable, and platforms is named when the body runs on one system, so elsewhere the reflex is inactive and says why.
  2. The body detaches what outlives the run, stops when signal aborts, fails by throwing in its own words, runs programs without a shell, and is one file, since the deadline of thirty seconds a decision, the kernel and a reviewer hold it to that. The body has the rest.
# kept: one program, named; the system it runs on
platforms = ["macos"]

[needs]
runs = ["lpstat"]

runs = ["sh"] reaches everything a shell can; a program this machine lacks refuses at check, whatever platforms names: [needs] runs names lpstat, which is not on PATH.

10. How to work

  1. Work in the order that finds each fault first.
    1. evoke new writes a reflex that checks clean, and evoke new --playbook a playbook.
    2. evoke check, after every edit, refuses what cannot load and reports lint, without the classifier, on a machine that runs the body.
    3. evoke trust once; then give your own project a word in each vocabulary the reflex requires, since a reflex over an empty vocabulary is inactive and test skips it.
    4. evoke add into that project beside the neighbours, which names each of their examples your reflex would take.
    5. evoke try one sentence per outcome you expect: a run, an ask, and one that is not yours.
    6. evoke test, which decides every record over everything installed.
    7. evoke teach a miss, and copy its line into [examples], dropping the vocabulary argument from what it asserts.
    8. evoke calibrate over what the project gathers.
  2. Read the contract diff before the tag, since a reworded step, a removed option or an argument made required is major. Rename an argument with was, so every user's overlay follows.
$ evoke check                                      # kept: nothing refused, the lint weighed
  timer  write  runs timer.mts
  0.4.0  same

A manifest with a key evoke does not read prints unknown timer: args.label.not_for is not a key this evoke reads and exits 0: the key is lost, and the manifest loads as if it were absent, so read every line.

A reflex handed as code

A reflex or a playbook handed to the SDK's load as code, reflex({…}) or playbook({…}), keeps every rule above except those about the body's files: its name is its key under reflexes, it has no [needs], [config] or platforms, and its body runs in your process and honours signal. load refuses what evoke check refuses in a manifest, and the plan refuses as it would for any reflex; nothing lints it, nothing names what it steals, and a key evoke does not read is dropped with no line printed. So run its records through a project's evoke test before you ship it, and read it with the checklist in hand.

What evoke checks for you

You need not remember these. The tool holds them itself.

evoke check refuses

  • A body that is missing or does not load; a program [needs] names that is not on PATH; an argv element no argument fills; a [needs] entry naming no argument or setting, or naming a flag; a host other than *; a path this machine lacks.
  • An argument with no source or two; a pick of no known kind; an argument with no ask; a range on anything but number and duration; recent beside anything but a pick; a reserved argument name; a retired argument name back in use, or one dropped from was.
  • No confirm; a confirm naming an optional argument, a flag, a taken result or no argument.
  • A record asserting a span the sentence does not hold, a key not offered, a taken argument or a vocabulary word in a shipped manifest; two records with one utterance.
  • A step that reads as a call with a value; more than 24 steps; an optional slot outside a bracket or a required one inside; an argument no step names; a when at step 1 or on two fields.
  • takes beside another key, on an argv body or a destructive reflex, or of a name the reflex returns; returns or [yields] on an argv body; a body's key on a playbook.

evoke check reports and never refuses

  • A key it does not read, which is lost.
  • A summary over 100 characters; a description over 1,000; more than 8 not_for lines; more than 24 options on one argument; more than 40 records in a table; a sentence or a step over 200 characters.
  • A phrase that addresses a model.
  • effect left out; fewer than three examples; an option or a pick that no record leaves out; a quoted argument that no example shows in quotes; a confirm that names no required argument, or asks Are you sure; an option's meaning that repeats its ask.
  • A step holding a connective, stating a channel or an address, or opening with check that; a step whose words are all the summary's or an example's.

The plan refuses before anything runs

  • A step no reflex matches; a step that takes a result no step before it returns, or two return.
  • A branch on a field its source does not yield, or a value no reader reads; a when step that reaches a playbook, or that a later step takes from.
  • A plan past 24 steps or three deep; a step reaching its own playbook.
  • A part of the person's sentence that opens with if, unless or in case; a part that says what not to do, beside a playbook's plan.
  • It stops rather than guess: a reference several fields could satisfy, or one item of a list, stops the plan before anything runs, naming them.
  • Every request stops while one question of the project offers more than 255 choices, unstated counted: a vocabulary past 254 words, an argument past 254 options, a project past 254 reflexes.

add and test report and go on

  • steals and also fits, each with the phrase and its owner; a claimed effect its steps pass; a step that reaches a reflex without the playbook's tag; a branch source that yields no such field; a step that reaches nothing here.
  • test fails a record that misses, and a step that reaches nothing or its own plan. It names a record that passes while its call would wait for a yes, since the call holds less than the record's own words.

Exceptions

  • A change that needs nothing. trash and screenshot need no value from the sentence, so only the gate stands before them. Their confirm names the action alone, Empty the trash?, and their not_for names what people type nearby, «logging out». (Rules 16, 23.)
  • A reflex whose only required values are taken. lock takes the laptop device found; its confirm names the action alone, Lock the laptop?, and its examples are the words people type for it inside a longer sentence. (Rules 2, 23.)
  • A gentler state beside a harsher one. States of one reflex share its effect, so the gentler one confirms as the harsher one does. Accept that price only where people type the action without its state; where they name it every time, the gentler state is a reflex of its own: sleep, a write, stands beside power, which restarts or shuts down. (Rule 1.)
  • A playbook fits its own steps. add prints also fits for the sentences of the reflexes a playbook's steps reach, since the plan does hold that action, and no not_for line takes that away. Such a sentence typed alone still reaches its reflex. (Rules 7, 41.)
  • One verb, two things. «restart the payments service» and «restart the laptop» share a verb across two collections; each names the other's object in not_for. (Rule 13.)
  • One exact sentence a neighbour owns. When one whole sentence keeps reaching a reflex past its not_for, it becomes a false example, "mute the sound" = false in volume. (Rule 13.)
  • What every team calls the same. A step may name a thing no team would rename, «update the status page for {service}». (Rule 27.)
  • A lint line is weighed. Lint flags the first word a step would be split on, so next in «find the next flights to {city}» is flagged though the step is one action. (Rule 24.)

Checklists

A line is answered from the files, unless it ends with what answers it: a command, the neighbours or your users. A no sends you to the rule it names.

Before a tag

  • evoke check refuses nothing, and you weighed every lint line. (45)
  • effect is written, and it is the effect the action has. (33)
  • The second line agrees with the effect. (11)
  • The summary says the action with what a person gives it, in the words people type. (11)
  • Each argument is one the body uses, with one ask and one source. (3)
  • Each optional argument has a default people mean. (3)
  • Options cover every value people mean; an open value is a pick; a switch is a flag. (4)
  • No quoted argument reads text people type without quotes. (21)
  • The confirm names each required value and nothing else. (23)
  • not_for names the near neighbours, none of them generic. (13)
  • Three examples at least, as people type them, each asserting the options, flags and pick spans it states and never a vocabulary word. (14, 19)
  • Each near neighbour is a false test in words no example uses. (15)
  • One record leaves out each option or pick argument. (15)
  • No record opens with a negation or a condition. (15)
  • [needs] is the tightest that runs; platforms is named when the body runs on one system; a secret is a variable's name. (43)
  • evoke test passes beside the neighbours. (45)
  • evoke add printed no steals and no also fits. (41)
  • Inside the repository, evoke check's last line, 1.2.0 → 1.3.0 minor, names the version you tag; a first tag has none. (46)

Before a playbook's tag

  • The situation comes back, and your team handles it with the same steps each time. (5, your users)
  • The summary and every example state the situation, and none names a step. (6)
  • The test is the short sentence people type for it, in words no example holds. (6)
  • not_for names its steps taken alone, in the words people type for them. (7)
  • Each step is one action, as typed, with no condition. (24, 9)
  • No step writes in a channel or an address. (27)
  • No step without its slot holds a word people give that vocabulary, search for a service. (25, your users)
  • Each slot stands in every step whose reflex reads it, an optional one in a bracket. (25, 26)
  • The lookups a change rests on come first. (8)
  • A later step refers to what an earlier one found with it, them or its noun. (28)
  • Each check is worded whether, its reflex yields the field, and one step per value follows it. (9, 28)
  • No step reads like the situation or like an example. (29)
  • effect is the worst step's. (10)
  • The confirm names the required slots. (23)
  • evoke add shows every step reaching the reflex you meant. (45)
  • evoke test fills every step from a record. (45)

A collection's shape

  • One line per reflex: its name, its effect, each argument's source, what it returns and takes. (37)
  • One reflex per action, named by the word people use, unique in the projects it joins. (1, 34)
  • Each change's lookup is its own reflex where a plan needs it, and yields what the change takes. (2, 30)
  • Each change people ask about has a lookup that answers. (39)
  • Every vocabulary name is on the list below, or names a kind of thing none of them holds. (19)
  • The things the user names are one vocabulary, or the collection has none. (38)
  • The near neighbours are listed, inside the collection and toward the collections it will join. (13)
  • No reflex covers an action a neighbour already covers. (39)
  • Every reflex a playbook's steps reach carries the playbook's tag. (40)
  • Each situation people type has a playbook, or a reason for none. (5)
  • The README's table holds the shape. (42)

Vocabulary names

A vocabulary is the user's list of words for one kind of thing, kept in vocab/<name>.toml. You name the list in vocab = "<name>"; the user writes its words, their meanings and, where the body needs one, their values. A collection never ships one. Reflexes share a list only by naming it alike, so use these names, and coin a new one only for a kind of thing none of them holds: a plural noun for what its words are, like vehicles or printers.

NameIts words areA word's value, when the body needs one
placesfolders on this machinethe folder's path
sitesweb sitesthe URL
roomsrooms of a homean id the lights know
peoplepeople you work withan address or an account id
teamsteamsan id
channelschat channels, written #incidentthe channel's id
servicesservices a team runsthe service's id
regionswhere a service runs, eu-westan id
citiescitiesa code
monthsmonths, septembernone
shipmentsshipments on their waya tracking id
supplierssuppliersan account id

A day, a time, an amount and a code are never a vocabulary. They are picks, read by code from what the person typed, tomorrow and 3pm among them.

A word's meaning:

  • names what the word is, in the words people use for it, since a sentence or a step that describes the thing reaches the word through its meaning: "#incident" = "Where an outage is handled.";
  • holds only what the word is, since every extra word draws near phrases to it: den = "The TV room downstairs; also 'the snug'." also draws «the living room», a room the list lacks;
  • holds what stays true, september = "September.", never «last month», since the meaning is read in every month;
  • never holds the value, since the meaning is read by the classifier and the value is not.

Drafting with a model

A model may write a first draft of a manifest, a playbook or a whole collection. A person reads every line before it is saved, and decides. Once saved, a manifest is words: the drafting model takes no part in any decision.

Give the model what the body does, what it touches and what goes wrong when it runs by mistake; the collection's shape, with each neighbour's summary and not_for; sentences your users typed for the action, so the examples start from real words; these rules and the checklists; what evoke check printed for its last draft.

Never give the model your tests, since a test proves a boundary only when its words were out of view while the manifest was written; nor a user's vocabulary values, a setting's value or a secret.

Then read the draft as its owner. The effect, the contract and the tag are your claims, never the model's: set effect yourself, since it sets the floor every call must clear. Check each example against the body: the values it asserts are the values the body would receive. Write the tests yourself, after the draft, in words the draft does not use. Run check, add and test on the draft as on any manifest, and read the lint line that names text addressed to a model. Read it with the checklist in hand: a fluent draft is easy to approve unread.

Next: The manifest, key by key; Arguments; Examples and tests; Playbooks; The body; Publishing.

Publishing

Publishing is a git tag. A reflex's identity is its location, owner/repo[/dir]. Its version is the tag it was fetched at. There is no account, no registry API, no build step, and no upload.

A repository of reflexes

radhi/home                        a repository is a collection
├── lights/{reflex.toml, lights.mts, reflex.d.ts}
└── blinds/{reflex.toml, blinds.mts, reflex.d.ts}
  • evoke add radhi/home installs every directory directly under it that holds a reflex.toml. evoke add radhi/home/lights installs one. Directories may nest: name a deeper directory, or the one above it.
  • A repository may also be a single reflex at its root. An author may keep a project inside that directory: evoke.toml, evoke.lock, overlays/, vocab/, evoke.d.ts. None of it ships. Those five names are skipped when a reflex is fetched and hashed.
  • Symlinks and submodules are refused at fetch. Everything a body needs is a committed file in its directory.
  • A LICENSE at the listed root is needed for a Hub listing. The collection uses MIT, since its scripts are meant to be copied into reflexes of your own.

Design a collection before you word its reflexes: rules 37 to 42.

Tags and versions

$ git tag v1.2.0 && git push --tags
  • Tags are X.Y.Z or vX.Y.Z. The newest is the highest. A repository without a version tag cannot be installed.
  • Published tags never change. The lock records the tag, the commit and the content hash. sync refuses a tag that moved. Publish a fix as a new tag.
  • One tag covers the whole repository. Every reflex in a collection moves together, and evoke check reads the contract diff for each from the same series.

What the next version must be

Run evoke check in a reflex directory inside a git repository. It diffs the manifest's contract against the repository's newest version tag, and says what the next tag must carry:

$ evoke check
  lights  write  runs lights.mts
  lint  lights: args.power has no record that leaves it out; assert power = false once
  lint  lights: args.brightness has no record that leaves it out; assert brightness = false once
  lint  lights: examples holds 2 requests; write three at least
+ reflex.d.ts
  1.2.0 → 2.0.0  major
    args.state  renamed power

Each lint line reports a rule the manifest breaks. Lint never refuses: What evoke checks for you.

LevelWhen
sameWording only: description, not_for, tags, confirm, any ask, option meanings, records. Also a declaration narrowed
minorAdditions, like an argument, an option, a config key, a field under [yields], a returns name or a platform. Also a config key removed, an argument made optional, a secret made plain, or a declaration widened
majorSomething a user's files or calls may not survive: an argument or option removed or renamed, a source, a range or recent changed, run or steps changed, an argument made required, a config key made secret, a yield or a returns name removed or changed, a taken argument added, removed or changed, a platform dropped

A was violation is refused outright. That means a retired name returning, or a name dropped from the list. With no repository, no tag, or no reflex at this directory in the tag, no diff prints.

Read this diff before every tag: rule 46. The checklists list what else to check before a tag.

Renaming safely

was lists an argument's former names, which stay listed forever:

[args.power]
was = ["state"]

Every user's overlay and call follows at merge time, and update tells them so. Rename an argument with was: rule 46. An option key has no was, and removing one is major.

What users see at update

evoke update never prompts, never blocks and never rewrites a user's files. It reports your rewritten description as taken, unless they overrode it. It reports each contract change, and what they override that you changed. It reports a loosened effect, which they keep until they accept it. A tightened effect applies at once.

What add checks

Each install lints the manifest, as evoke check does. It also routes every installed example over the new set. A steals line names an example your reflex would take from the reflex that owns it. An also fits line names an example your reflex also fits: two reflexes claim the sentence, and one worded like it may reach either. For a playbook, add names each step whose reflex lacks the playbook's tag. None of these lines stops the install.

Before a tag, install your reflexes beside the ones they will live with, and read every such line: rules 40 and 41.

The Hub

The Hub is where reflexes are found. Until about twenty third-party reflexes exist, it is the first-party collection, evoke-build/reflexes, reviewed and published as one repository. After that, it becomes one index repository. A listing is a one-line pull request. Static pages at evoke.build show a preview, a diff, and a search over examples. No install ever depends on it. add and sync reach git directly.

The Hub publishes no per-engine pass rates. An engine's terms may forbid them.

Next: The collection, or the SDK.

Getting started with the SDK

@evoke-build/evoke loads a project of reflexes, decides what an input asks for, and runs it. It does this inside your application, with the same rules as the CLI. The rules run in core.wasm, the CLI's own core. The SDK adds files, the network and a process.

Six things to learn, in order: reflex() · load() · handle() · a Decision · run() · replay().

Install

$ npm install @evoke-build/evoke

You need Node 24.5 or newer, and ES modules: "type": "module" in your package.json, or .mts files. The package ships five entries, so the main one imports no engine:

ImportHolds
@evoke-build/evokeload, reflex, the types, the errors
@evoke-build/evoke/jevjev(), the first adapter
@evoke-build/evoke/openjevopenjev(), the same model through OpenJEV, an independent service
@evoke-build/evoke/clefclef() and clefFlash(), Cloudflare's Clef on Workers AI
@evoke-build/evoke/testingreplay(), the recorded adapter for tests

The first hour

One file. A reflex handed as code, a project with nothing on disk, and the whole loop in one call:

// app.ts · export TYPESAFE_API_KEY=… · node app.ts "timer for 10 minutes"
import { load, reflex } from "@evoke-build/evoke"
import { jev } from "@evoke-build/evoke/jev"

const timer = reflex({
  description: "Start a countdown timer.",
  effect: "write",
  confirm: "Start a {duration} timer?",
  args: { duration: { ask: "How long?", pick: "duration" } },
  examples: { "timer for 10 minutes": { duration: "10 minutes" } },
}, async ({ duration }) => {                                   // duration: number — seconds, typed from the manifest
  setTimeout(() => console.log("ring"), duration * 1000)
  return `ringing in ${duration} s`
})

const project = await load({ reflexes: { timer }, adapter: jev() })

const handled = await project.handle(process.argv[2] ?? "", {
  confirm: async d => { console.log(d.prompt.template); return true },   // "Start a 10 minutes timer?"
  ask: async d => ({ duration: "10 minutes" }),                           // d.missing says what is missing and why
})
console.log(handled.outcome === "ran" ? handled.result.text : handled.outcome)
  • reflex() takes the manifest as an object, and the body. The object is the file's shape, without run and config. The body's argument types are inferred from the manifest literal.
  • load() builds a project. Here it builds one from code alone, with the adapter passed in.
  • handle() runs the whole loop with your two handlers: decide, ask until nothing is missing, confirm, run. It returns ran, abstained, declined or unanswered.

With files

Next to an app, evoke add evoke-build/reflexes writes evoke.toml, evoke.lock and evoke.d.ts, exactly as it does in your home project. Overlays and vocabularies under the same root tune the wording. That includes a reflex handed as code.

import type { Reflexes } from "./evoke.d.ts"

const project = await load<Reflexes>({ root: import.meta.dirname })       // the adapter evoke.toml names
const d = await project.decide("kill the lights in the den")
if (d.outcome === "run" && d.reflex === "lights") console.log(d.values.room, d.values.state)   // typed by evoke.d.ts

A Decision is a union. Narrow it by outcome, one of run, confirm, ask and abstain, then by reflex.

Tests, offline

import { replay } from "@evoke-build/evoke/testing"

const record = process.env.RECORD ? jev() : undefined                     // RECORD=1 asks Jev once and writes the file
const project = await load({ reflexes: { timer }, adapter: replay("answers.toml", { record }) })

Every run after the first is offline, and always gives the same answers.

Next: Projects in code.

Projects in code

load() reads a project once and compiles it. A project is the owned files under a root, the reflexes handed as code, and who answers. load() never fetches and never writes.

load(options)

const project = await load<Reflexes>({ root, reflexes, adapter })
OptionMeaning
rootThe project directory: evoke.toml, evoke.lock, overlays/, vocab/, local reflexes. Absent: no files at all. A root without evoke.toml is the default project, adapter = "jev", nothing installed
reflexesReflexes handed as code, by local name. A name evoke.toml also lists is an error. Overlays under root apply to them too
adapterWho answers. Absent: the adapter evoke.toml names, resolved from the SDK's own subpath and built under [adapters.<name>]. Required when there is no root
platform"linux" or "macos": the platform a manifest's platforms is judged by, this machine's when absent. A reflex named for another platform is inactive; name one to decide or test as that machine would. A body still runs here

Remote reflexes come from the store, by the lock's content hash, and are hashed again at load. A missing entry fails, and the fix is evoke sync. The SDK never checks trust, since the root is yours to choose. It keeps no cache.

Reflexes as code

const timer = reflex({
  description: "Start a countdown timer.",
  effect: "write",                                                   // absent means destructive
  confirm: "Start a {duration} timer?",                              // required
  args: { duration: { ask: "How long?", pick: "duration" } },
  examples: { "timer for 10 minutes": { duration: "10 minutes" } },
}, async ({ duration }, { input, signal }) => startTimer(duration))

The manifest is the file's shape without run and config, yields and returns included. A body closes over what it needs. Its types are inferred from the literal: an option key union, a word or a quoted pick as string, a number or a duration as number, a flag as true, an optional argument as optional, and an argument written { takes: "deploys" } as unknown, an earlier step's whole result the weave hands the body. So the body's args are typed with nothing generated. A manifest that does not read throws at load, since it is your own code. Its problems end in reflex(<name>). The body runs in-process. It is your application's code, unscrubbed, and nothing can end it from outside. So it honours the deadline's signal.

playbook(manifest) hands a playbook the same way: steps in the manifest and no body (Playbooks). Its steps are decided over the project's reflexes when a sentence reaches it, and it reports runs: "plan".

Generated types

evoke add, update and remove write evoke.d.ts next to evoke.toml. It holds one Reflexes interface, with a member per installed reflex, and its arguments as a decision carries them. Hand it to load:

import type { Reflexes } from "./evoke.d.ts"
const project = await load<Reflexes>({ root: import.meta.dirname })

Now d.reflex narrows d.args and d.values. A project with reflexes both installed and handed as code writes load<Reflexes & ReflexesOf<typeof own>>({ root, reflexes: own }). TypeScript infers all type arguments or none.

What a project knows

project.reflexes   // per name: { active: true, effect, runs: "inline" | "file" | "argv" | "plan" } | { active: false, problems }
project.plan       // the digest of the compiled set, "h1:…"; every decision carries it

An inactive reflex is left out of every decision. The causes are an empty vocabulary, a setting unset, or an overlay that does not parse. Its problems each end in the command that fixes it.

with({ vocab }): a tenant's words

A vocabulary binds a project, not a call. with compiles the same set over vocabularies of its own, in the file's form. It is synchronous and takes milliseconds:

const tenant = project.with({ vocab: { rooms: await db.rooms(user) } })
const d = await tenant.decide(input)
if (d.outcome === "run") await tenant.run(d)

decide, fill and run on that project cannot disagree about the plan the decision was made under. So a multi-tenant server stays correct. A decision made on one tenant's project is refused by another's.

Running bodies from the SDK

A reflex the project names, by a git ref or a directory, runs in a child process, through the SDK's own loader, under the SDK's Node, held to what its manifest declares under [needs] as far as the package can: under Node's permission model on both systems, and under Seatbelt on macOS. An argv reflex is spawned directly, under the same layers. On Linux the kernel's layer needs native code the package does not carry, so a file body runs partly held and an argv body unheld there, and the result's contained says which, in the shape --json prints. A reach past the declaration ends the run with what was reached and the key, as it does in the CLI. Nothing starts before the decision. The body's deadline is 30 seconds, less what the adapter call took.

Next: Decisions.

Decisions

One input decided is a Decision. It is a union, narrowed by outcome and then by reflex. decide makes one. fill answers an ask. run runs a chosen call. handle does all of it.

decide(input, { tags?, signal? })

const d = await project.decide("kill the lights in the den", { tags: ["home"] })

Asks the adapter, reads its answers, and gates them. Runs nothing. tags offers only the reflexes carrying one of them. signal aborts the adapter call, and decide rejects with the signal's own reason.

The four shapes

outcomeCarries
"run"reflex, args, values, call, effect, confidence, weakest, judgments, contenders, runner_up?, whole?
"confirm"All of the above, plus prompt: { own, reason, template } and because: Cap[]: every reason it stopped, in order
"ask"reflex, partial args and values, unconsumed, missing: Missing[], and the judgments so far
"abstain"contenders, the ranking, and judgments

Every decision also carries three more things. input is the sentence as decided. plan is the digest it was decided under. trace has one entry per adapter call: { adapter, questions, ms }.

switch (d.outcome) {
  case "run":     await project.run(d); break
  case "confirm": if (await ui.confirm(d.prompt.template)) await project.run(d, { confirmed: true }); break
  case "ask":     project.fill(d, await ui.pick(d.missing)); break
  case "abstain": ui.say(d.contenders); break
}

args and values

args is what the classifier read, typed: { type: "option", key }, { type: "word", word, value? }, { type: "pick", span, value }, { type: "flag" }. values is what the body receives: the key, the word's value or else the word, the number or text, true. With Reflexes from evoke.d.ts, or a manifest handed as code, both are typed per reflex once d.reflex is known:

if (d.outcome === "run" && d.reflex === "lights") {
  d.values.room    // string
  d.values.state   // "on" | "off" | "dim"
}

An app that wants a reflex's wording with a body of its own calls decide, switches on d.reflex, and reads d.values.

Confirm

prompt.own is evoke's line: the call, the effect and the weakest judgment. prompt.reason says in one line why the call waits, a phrase for each cap, as the CLI prints it under the call. prompt.template is the reflex's own question, filled in. because lists why it stopped: destructive, no_gate, under_floor, one_view, respelt, text_read, more, cut, quoted, text_left, whole, detail, named, pointed (Outcomes). A confirm decision runs only with run(d, { confirmed: true }).

decide reads the input as one call. When its words say the call is wanted once more, like too or the same for, the decision is a confirm with more, since there is no second step to be the other time. steps reads every step of a sentence.

Ask and fill(d, given)

missing says, per argument, its ask, why it is missing, and what it may be. Missing means never stated, or a pick out of range. What it may be is the options, the vocabulary's words, or which recognizer reads it, with choices.recent listing the values recalled from the results the application handed decide, handle or weave as recent: [{ reflex, data }], newest first, when the pick's manifest names the field. Where the input's words say a pick's value aloud and read in more than one way, words holds them and choices.readings lists what they read as: offer those in place of the recalled values. For a word from a vocabulary, likely names the listed word to offer for a yes before the choices, which keep their order. fill takes what a person answered, by argument name: an option's key, a word, or the text a pick reads, a recalled value or a reading among them. It types the answer and gates again, synchronously:

const filled = project.fill(d, { room: "den" })       // a Decision again: run, confirm, or still an ask

A text that does not read is a DiagnosticError naming the argument and what it may be: room: "attic" is not one of den, office. A flag is never asked.

run(d, { signal? })

Runs the chosen call's body and resolves to { text, data? }, with contained when a file or an argv ran: whether this machine held it to its declaration, in the shape --json prints. A reflex handed as code runs in-process. A file runs in a child. An argv is spawned. A body's failure is a FailureError. signal aborts the body, and run rejects with the signal's reason. A decision made under another plan is refused as misuse, with a TypeError. A reflex that takes an earlier step's whole result is refused with a DiagnosticError: only weave hands one, so a host with its own loop learns at run that decide's decision was a taker's. A playbook is refused the same way, <name> is a plan of steps; say it in a sentence: only steps and weave make its plan.

handle(input, { confirm?, ask?, tags?, signal? })

The whole loop: decide, then ask and fill until nothing is missing, then confirm, then run.

const handled = await project.handle(input, {
  confirm: d => ui.confirm(d.prompt.template),   // true runs, false declines
  ask: d => ui.pick(d.missing),                  // answers by name, or undefined to decline
})
handled.outcomeWhen
"ran"The body ran: decision and result
"abstained"none won, or the route was under its floor
"declined"confirm returned false, ask returned undefined, or an answer changed nothing
"unanswered"A confirm or an ask was reached and no handler was given. The decision itself is the answer

An answer that does not read is asked again once. Twice is a decline. An empty input is refused before the adapter is asked. A decided reflex that takes an earlier step's whole result is refused before any confirm, and so is a playbook. Only a diagnostic, a fault or a failure throws.

steps(input, { tags?, signal? }) and weave(input, { confirm?, ask?, proceed?, tags?, signal? })

A sentence that asks for several things is a weave (Weaving). steps reads one into its plan, runs nothing, and returns it sealed as a plan file — the sentence, the plan, every answer the classifier gave, and the pins it was decided under — the object evoke try --save writes and evoke run <file> reads (The plan file):

const pinned = await project.steps("look up dana's address and email them")
const plan = pinned.weave
plan.steps       // [{ n: 1, text, decision, reflex: "contact", effect: "read", ... }, { n: 2, ... }]
plan.binds       // [{ from: 1, to: 2, arg: "to", field: "email", kind: "email", via: "fill" }]
plan.stages      // [[1], [2]]: a stage's steps run together; stages run in order
plan.verdict     // { outcome: "run" | "ask" | "confirm" | "refuse", because?: [...] }
pinned.set       // the plan digest it was decided over; pinned.answers, every answer the plan took

Each step carries its decision, the Decision decide would have made of its words alone; shared names the words the sentence said once for several steps that reached it, { service: { word: "checkout", via: "fill" } }; repair says how a part that was not decided on its own became a step, "narrowed", "spliced", "merged", "split", "corrected", "named", "first", "verb", "again" or "apart" (The JSON line). A binding whose via is "takes" is a whole result by the name its source returns, with no kind; a plan with no source for one, or two, has verdict.outcome "refuse" and no_source or several_sources among its because. A step a playbook wrote carries from, the playbook, its step and the slots the sentence filled (Playbooks); folded names the parts of the sentence that repeat one of its steps; and the verdict's because holds reviewed per playbook, with the sentence's own prompt. steps(input, { ask }) answers a slot the sentence lacks before the plan is made, as weave's ask does; such a plan is a sheet, and weave refuses it as a file, since a file holds no one's answers. weave settles what the plan asks first, then runs every step under handle's handlers, each told which step and round asks, a Turn:

const woven = await project.weave(input, {
  ask: (d, turn) => ui.pick(d.missing, turn.step),   // before anything runs, for a required argument no step provides
  confirm: (d, turn) => ui.confirm(d.prompt.template), // at the step's turn
  proceed: plan => ui.confirm("Run the plan as it stands?"),  // a step refers to another it takes nothing from
})
woven.status     // the worst step's: "ran", "failed", "declined", "refused", "unanswered"; or why nothing ran
woven.steps      // per step: { step, status, why?, bound, rounds: [{ round, input, decision, status, result? }] }

A confirm handler may wait: hand the decision, plain JSON, to a queue a second person reads, and resolve with their answer; the weave goes on where it waited, and nothing that ran runs twice. The whole plan may travel instead: JSON.stringify(pinned) is the file, and weave(pinned, { proceed, confirm?, ask?, signal? }) runs it on the same project, here or on another machine — its pins checked first, a DiagnosticError naming the first that moved and the call that makes the plan again; the plan made again from the file's answers under its gate and refused when it does not read the same; then proceed(pinned), required whatever the verdict, since a file's numbers are whoever wrote them; then every step under the handlers, a value bound at run time decided by this project's adapter. A plan that asks or refuses runs nothing.

const checker = await load({ root, adapter: jev() })
const woven = await checker.weave(JSON.parse(file) as Pinned, {
  proceed: pinned => ui.confirm(`Run the plan of ${JSON.stringify(pinned.input)}?`),
  confirm: (d, turn) => ui.confirm(d.prompt.template),
})
``` A body's failure is the
step's, with `status: "failed"`, never a throw. A step after one that stopped is `skipped`, with `why: { type:
"earlier_step" }`. A step bound to a list of records runs once per record, one
round each. A step that takes whole results receives them in its body's `args`, handed by the weave alone; its
`bound` entries name each argument, its source and the name, with no `value`.

`signal` aborts the adapter's call while the plan is made, as it does in `decide`, and cancels the run once it
is under way: every body is ended, its own `signal` aborted first, nothing starts after, and every step that did
not finish reads `skipped` with `why: { type: "cancelled" }`. `weave` then rejects with the signal's reason, the
record on it as `woven`:

```ts
try {
  await project.weave(input, { signal: controller.signal })
} catch (error) {
  if (error === controller.signal.reason) console.log(error.woven.steps) // what ran, and what was cancelled
  else throw error
}

Next: Adapters.

Adapters

An adapter is the classifier behind a decision. It is an object that answers typed questions with probabilities. The core names no engine. A project names an adapter, and only your machine resolves the name. Two built-in adapters reach Jev: jev(), through TypeSafe AI's own API, and openjev(), through OpenJEV, an independent service. They send the same questions and ship the same bars. Two more reach Clef, Cloudflare's model: clef() and clefFlash(). They send the same questions and ship no bars.

jev(): the first adapter

import { jev } from "@evoke-build/evoke/jev"

const project = await load({ reflexes, adapter: jev() })
const project = await load({ reflexes, adapter: jev({ key, gate: { write: 0.95 } }) })
OptionMeaning
keyThe API key. Absent: TYPESAFE_API_KEY from the environment, read when jev() is called
gateFloors over the defaults of route 0.5, fits 0.3, read 0.8, write 0.9 and whole 0.3. The same as [adapters.jev] gate in evoke.toml

When load resolves jev by name from evoke.toml, it is built under the file's [adapters.jev] table. An adapter passed in replaces both. The transport keeps one connection alive for the process, through the proxy HTTPS_PROXY names when one is set; a proxy needs Node 24.5 or newer, and jev() says so on an older one. It retries once after a connect error or a server error, and never after a client error, except a 429 that says how long to wait: that wait is waited out, and the request sent again, within the deadline. No key is a DiagnosticError ending in export TYPESAFE_API_KEY=<value>.

openjev(): the same model, through OpenJEV

import { openjev } from "@evoke-build/evoke/openjev"

const project = await load({ reflexes, adapter: openjev() })
const project = await load({ reflexes, adapter: openjev({ key, gate: { write: 0.95 } }) })
OptionMeaning
keyThe API key. Absent: OPENJEV_API_KEY from the environment, read when openjev() is called
gateFloors over the same defaults as jev(). The same as [adapters.openjev] gate in evoke.toml

OpenJEV is an independent service, not TypeSafe AI's. It forwards each request to Jev and returns Jev's answers. evoke is affiliated with neither, and vouches for neither. A key from OpenJEV is on OpenJEV's terms, and whether it may offer Jev is a matter between OpenJEV and TypeSafe AI. As of this release it publishes no terms and no privacy policy, so weigh that before you choose it for an application. The transport is jev()'s, with 3 seconds once connected instead of 1.5, since the service forwards the request onward. It names the model by version, jev-1.13.0, as jev() does, so its id is Jev's own. No key is a DiagnosticError ending in export OPENJEV_API_KEY=<value>.

clef() and clefFlash(): Clef, on Cloudflare's Workers AI

import { clef, clefFlash } from "@evoke-build/evoke/clef"

const project = await load({ reflexes, adapter: clef() })
const project = await load({ reflexes, adapter: clefFlash({ key, account, gate }) })
OptionMeaning
keyThe API token. Absent: CLOUDFLARE_API_TOKEN from the environment, read when the adapter is called
accountThe account's id. Absent: CLOUDFLARE_ACCOUNT_ID from the environment, read at the same time
gateThe floors route, read and write, with fits and whole if you want them. The same as [adapters.clef] gate or [adapters.clef_flash] gate in evoke.toml

Clef and Clef-flash are Cloudflare's models, with weights published under Apache-2.0. clef() reaches Clef and clefFlash() the smaller Clef-flash. Both run on Cloudflare's Workers AI, in the account the token belongs to. Cloudflare's terms cover the token and each request.

These adapters ship no floors, so every decision confirms. To let calls run, pass a gate with route, read and write, and measure the numbers you choose on your own records: Calibrating. A gate that lacks one of the three is a DiagnosticError.

The account's id goes into the address. So it must be 32 hex digits in lowercase, and anything else is refused before a request is made. The transport is jev()'s, with 6 seconds once connected for clef() and 3 for clefFlash(). Workers AI takes 64 questions a request, so a longer request is sent in parts and answered as one. It names no version of a model, so each id carries a date: clef-2026-10-03 and clef-flash-2026-10-03. No token or no account is a DiagnosticError ending in the export line for its variable.

The contract

interface Adapter {
  id: string                                           // opaque; changes whenever answers could; compared, never parsed
  limits?: { options?: number; tokens?: number }       // options: a plan over it is refused at load; tokens: declared, not enforced
  gate?: { route: number; fits?: number; read: number; write: number; whole?: number }   // each number means P(correct)
  plan?: string                                        // a recording's plan digest; another plan refuses it
  answer(state: { request: string }, questions: Record<string, Question>, signal: AbortSignal): Promise<Raw>
}

type Question =
  | { type: "choice"; ask: string; options: Record<string, Text>; otherwise?: string }   // the key meaning "none of these"
  | { type: "yesno";  ask: string; yes: Text; no: Text }                                 // answers { yes: p }
type Text = string | { what: string; not_for?: string[]; examples?: string[] }
type Raw  = Record<string, Record<string, number>>    // per question, a number per key
  • State is only { request }. An adapter sees the input and the questions, nothing else.
  • A choice answer is a distribution over the keys offered, summing to 1. An omitted key reads as 0. A yes/no answers { yes: p }. The core validates every answer and fails closed. A missing question is a fault.
  • otherwise marks the sentinel, none or unstated, in the structure. So an engine may abstain its own way.
  • gate is the adapter's own calibration. The core never rescales. Without one, every decision confirms. gate.whole is the floor of a call read back against the input; without it no call is read back. gate.fits is read when a reflex is added: a new reflex that fits another's example at or above it is named.
  • answer is stateless and may be called with any subset of a plan's questions. signal aborts it at the deadline. An adapter that ignores its signal is raced against it anyway.
  • id covers everything that could shift answers: model, version, prompt rendering, calibration. It is pinned in the lock with the project's adapter name.

Writing one

const mine: Adapter = {
  id: "my-classifier-2",
  gate: { route: 0.5, read: 0.8, write: 0.9, whole: 0.3 },
  async answer(state, questions, signal) {
    const raw: Raw = {}
    for (const [id, q] of Object.entries(questions)) raw[id] = await classify(state.request, q, signal)
    return raw
  },
}
const project = await load({ root, adapter: mine })

Any engine that can answer a closed choice with probabilities fits. That could be a hosted model that spells them out, a local model with log-probabilities, or a zero-shot classifier. An embedding router cannot fill arguments on its own. A throw that is not evoke's own becomes a transport FaultError. The caller's abort passes through as its own reason.

Next: Testing.

Testing

replay() is an adapter over a recording. The recording is the CLI's own answers.toml, read and written through the core. With record, an utterance the file lacks is asked of a real adapter once, and written back. So one run records a test suite. Every run after is offline, and always gives the same answers.

replay(file, { record? })

import { load, reflex } from "@evoke-build/evoke"
import { jev } from "@evoke-build/evoke/jev"
import { replay } from "@evoke-build/evoke/testing"

const record = process.env.RECORD ? jev() : undefined
const project = await load({ reflexes: { timer }, adapter: replay(new URL("answers.toml", import.meta.url), { record }) })

const d = await project.decide("timer for 10 minutes")
assert.equal(d.outcome, "run")
  • RECORD=1 node --test asks Jev for every utterance the file lacks. It writes the file whole, through a rename, so a reader never sees half a recording.
  • Without record, a missing utterance is a FaultError naming it, with the call that records it.
  • The recording carries the adapter's declaration: id, limits, gate. So the gate in tests is the gate in production. A plan line, when present, pins the installed set the answers were recorded against. A project over another set refuses it.

The file

Answers are keyed by utterance identity: lower-cased, whitespace collapsed, trailing punctuation dropped. Each question is keyed by its id: route, <reflex>.<argument>, and weave.<name>, a question evoke asks on its own account. fits.<reflex> is asked when a reflex is added.

id = "replay"

[gate]
route = 0.5
fits  = 0.3
read  = 0.8
write = 0.9
whole = 0.3

[answers."kill the lights in the den"]
route                      = { lights = 0.95, timer = 0.01, volume = 0.01, none = 0.03 }
"lights.room"              = { den = 0.92, office = 0.03, unstated = 0.05 }
"lights.state"             = { off = 0.94, on = 0.03, dim = 0.02, unstated = 0.01 }
"lights.brightness"        = { unstated = 1.0 }
"timer.duration"           = { unstated = 1.0 }
"timer.label"              = { unstated = 1.0 }
"volume.level"             = { unstated = 1.0 }
"weave.view_lights__room"  = { den = 0.92, office = 0.03, unstated = 0.05 }
"weave.view_lights__state" = { off = 0.94, on = 0.03, dim = 0.02, unstated = 0.01 }
"weave.is_lights__state_1" = { yes = 0.95 }
"weave.left_lights_0_2"    = { action = 0.9, courtesy = 0.1 }
"weave.whole_693291516"    = { whole = 0.9, less = 0.05, other = 0.05 }

A pick's candidates are keyed <start>-<end> by their character offsets in the input. A recording made with record holds every question. You can write one by hand too: a question the file lacks is named by the fault, weave.whole_693291516 was not answered, and its answer is one line more.

The CLI on a recording

The same file drives the CLI. Set adapter = "replay" in evoke.toml, and the file's path in EVOKE_ANSWERS. That is how evoke's own transcripts run offline. It is also how a CI job can exercise a project without a key.

$ EVOKE_ANSWERS=answers.toml evoke try "kill the lights in the den"

Testing bodies

A body is a function. Import it and call it with a context of your own. No adapter is involved:

import note from "../note/note.mts"
const context = { input: "", config: { file: "notes/today.txt" }, signal: new AbortController().signal }
assert.equal(await note({ text: "buy milk" }, context), 'noted "buy milk" in ~/notes/today.txt')

Next: Errors.

Errors

Every error evoke raises is data with a fix attached. Its message is the CLI's own line: lights: vocabulary "rooms" is empty → evoke vocab rooms add <word> "<meaning>".

import { DiagnosticError, EvokeError, FailureError, FaultError } from "@evoke-build/evoke"
ClasskindMeansCLI exit
DiagnosticError"diagnostic"Something a person fixes in the files or the environment: problems, each { reflex?, at?, message, fix, command }3
FaultError"fault"The adapter failed, or its answers did not validate: fault, one of transport, status, refused, retired, unanswered, malformed, unrecorded4
FailureError"failure"A body or the machine failed: what, why, fix; a body's own error is the cause, and the frames of an error a file body threw are frames1

All three extend EvokeError, which carries command. That is the literal line that fixes it. It is empty when the only fix is to try again. fix is the structured value, and command is its rendering. Render nothing yourself.

try {
  await project.handle(input, handlers)
} catch (error) {
  if (error instanceof DiagnosticError) for (const p of error.problems) log(`${p.message}  →  ${p.command}`)
  else if (error instanceof FaultError) retryLater(error.fault)
  else if (error instanceof FailureError) log(error.message)
  else throw error
}
  • Misuse is a plain TypeError. That means running a decision that is not run or confirm, running a confirm without { confirmed: true }, or handing a decision to a project other than the one that made it.
  • Aborts reject with the signal's own reason, never wrapped; a cancelled weave puts its record on it as woven.
  • A bug in evoke, a trap in the core, is a plain Error naming it.

Next: the Reference.

Commands

Every command, flag and exit code, as evoke --help prints them and with what each one does. The three groups are use, tune and install; the SDK has its own reference.

evoke 0.21.0 · you invoke a function; you evoke a reflex

use
  evoke "<input>"                         decide, gate, run
  evoke                                   the REPL; a piped line is one input
  evoke try "<input>"                     decide only, and show every judgment
    --json                                one JSON line per input, for a filter
    --tag <tag>                           only the reflexes carrying the tag
    --save <file>                         the plan as a file, for run
    --                                    the rest is input, even a command word
  evoke why                               the last sentence, explained
  evoke run <call>                        by name, without the classifier
  evoke run <file>                        a saved plan, run as it stands
    --json                                one JSON line: the call and its result
    <call> is <name> [<arg>=<value> | <flag>]…

install
  evoke add <ref>… [--as <name>]          fetch, lint, lock, install
  evoke remove <name>
  evoke update [<name>]                   each remote reflex to its newest tag
  evoke update --accept <name>            a moved contract or effect, accepted
  evoke sync                              the lock realised on this machine
  evoke trust                             this project, trusted at its content

tune
  evoke show [<name>]                     the installed reflexes, or one as used
  evoke teach ["<utterance>"] <call>      the utterance means this call
  evoke teach ["<utterance>"] not <name>  the utterance is not this reflex
    --forget <name>                       the overlay's line for it, removed
    ["<utterance>"] omitted means the last input
  evoke vocab <name>                      the words and their meanings
  evoke vocab <name> add <word> "<meaning>" [--value <v>]
  evoke vocab <name> remove <word>
  evoke config <name> <key> <value>       a setting; a secret as --env <VAR>
  evoke test [<name>]                     every example and test, judged
  evoke calibrate [<name>]                the confidence measured on the records
    --repeat <k>                          each input decided k times: the spread
    --json                                one JSON object, for a script

author
  evoke new <name>                        a working reflex from the template
    --playbook                            a playbook instead: a plan of steps
  evoke check                             lint, types, contract against its tag

exit    0 ran · 1 failed · 2 declined · 3 needs a human · 4 adapter failed
manual  https://evoke.build/manual/

Grammar

  • The first argument selects a command only when it is exactly a command word. Otherwise every argument is the input, bare words joined by one space. -- forces input. Lines from stdin are always input.
  • --help, -h or help, and --version or -V, are flags in the first place. They print to stdout and exit 0. --help or -h right after a command word prints the help too.
  • A command word after a flag, evoke --json try "…", is refused with the way to write it. It is never decided.
  • What you asked to see goes to stdout: a reflex's result, the --json line, and the answer of show, vocab, why, try, test and check. What evoke says about a run goes to stderr: the call and its confidence, prompts, the line of a write, the ranking after an abstain, and every diagnostic.
  • Commands validate every argument before acting. Arguments that spell no command end in evoke --help. The reserved words edit, search, publish and adapter are refused by name until they exist.
  • A call is name arg=value…. A value is bare or a JSON string, like duration="10 minutes". A flag is its bare name. run and teach take a call as one argument or as separate words. An argument to run whose first word is no reflex name, month.plan.json or ~/month.plan.json, is a plan file.

Use

CommandDoesExit
evoke "<input>"Decides, gates, runs. Prints the call and confidence on stderr, and the result on stdout. Logged0 · 2 · 3 · 1 · 4
evokeOn a terminal, the REPL: > , line editing, history. Piped, a filter: one input per line, and the first non-zero exit is keptas each line
evoke try "<input>"Decides only, and prints the sentence as it was read: how it was read as a whole, then each step with which reflex, each value with what it stands on, the call and what would become of it, and why; a sentence of several steps shows the plan first. Last, how much the adapter was asked. Never logged: Outcomes0 · 3 · 4
evoke try --save <file> "<input>"The plan's lines, then the plan written as a file for run <file>: the sentence, the classifier's answers and the pins it was decided under, in clear. A plan that asks is refused with the step's question, exit 3; one that refuses, exit 2; nothing written either way. Never logged: Weaving0 · 2 · 3 · 4
evoke whyThe last logged sentence, shown as try would show it, and what became of each step: ran, ran at your yes, declined, asked, refused, failed, cancelled, skipped, unanswered; from <file> where a plan ran from one; a playbook's own block first where one wrote the plan0 · 3
evoke run <call>Runs the call by name. No classifier. The effect policy is kept, so a destructive call confirms with [y]es [n]o. A reflex that takes an earlier step's result is refused: only a sentence of several steps hands one. So is a playbook: a sentence reaches it. --json prints the call and its result as one line. Not logged0 · 2 · 3 · 1
evoke run <file>Runs a plan try --save wrote, exactly: a file, since its first word is no reflex name. A pin that moved refuses it with the line that names it and the command that mends it; so does a file that does not read the same as its answers. The plan prints, one yes over it is asked whatever its verdict, then each step at its turn as a sentence's is. Every step's line names the file. Logged0 · 1 · 2 · 3 · 4
FlagWithDoes
--jsoninput, try, runOne JSON line per input, per step of a weave, or per call; try prints a weave's plan whole: The JSON line
--tag <tag>input, tryOnly reflexes carrying the tag; repeatable
--save <file>tryThe plan written as a file for run <file>; one input, never stdin
--inputThe rest is input

Install

CommandDoes
evoke add <ref>… [--as <name>]Fetches each ref at its pin or newest tag, once per repository; reads a local ref, ./dir, where it is. Lints, tests for stolen phrases, writes evoke.toml, the lock and evoke.d.ts, and records the runtime; for a playbook, decides each step over the set it joins and prints what it reaches, and any reflex reached that lacks the playbook's tag. --as names a single ref
evoke remove <name>Drops the reflex from evoke.toml and the lock. Keeps your overlay, vocabularies, settings and the store's copy
evoke update [<name>]Moves each unpinned remote reflex, or one, to its newest tag. A pinned one moves to its pin. Reports, or prints up to date. Never prompts or rewrites your files
evoke update --accept <name>Takes on an effect upstream loosened, or a declaration it widened, at the current tag
evoke syncPlaces every locked reflex in the store at its locked tag, and records the runtime, or prints up to date. Never changes the lock
evoke trustTrusts the project here, at the content of its four owned paths

A ref is owner/repo[/dir][@tag] on GitHub, <git url>[#dir][@tag] over https or ssh, or ./dir for a local reflex: Installing reflexes.

Tune

CommandDoes
evoke showEvery installed reflex: name, ref and tag or ./dir, effect, what it runs or a plan of 7 steps. Then the inactive lines
evoke show <name>The effective manifest as TOML, with + in the gutter of every line that is yours; for a reflex that takes whole results, which installed reflexes return each; for a playbook, its steps after confirm, numbered, a step that may not run ending in its when. Then its inactive lines
evoke teach "<utterance>" <call>Writes the example to overlays/<name>.toml, with only what the call asserts
evoke teach "<utterance>" not <name>Writes "<utterance>" = false to the reflex's overlay
evoke teach --forget "<utterance>" <name>Removes the utterance's line from the reflex's overlay, an example or a test
evoke teach <call> · evoke teach not <name>The same, for the last input the log holds; of a weave, the step the lesson's reflex decided. A first word is the utterance when a call follows it, unless it names an installed reflex
evoke vocab <name>Lists the words. Empty exits 3, with the add line
evoke vocab <name> add <word> "<meaning>" [--value <v>]Adds or replaces a word
evoke vocab <name> remove <word>Removes one. A word that is not there is refused
evoke config <name> <key> <value>Sets a declared setting under [config.<name>]
evoke config <name> <key> --env <VAR>Names the variable a setting is read from. The only way to set a secret
evoke test [<name>]Decides every example and test of every active reflex, or of one, without the cache; a playbook's steps too, each filled from a record, 7 steps route or the step that reaches nothing, or reaches a reflex that yields no field a branch waits on. A case that passes while its call would wait for a yes, holding less than its words, is named under its reflex and fails nothing. Exits 1 when a case failed; nothing to test says so
evoke calibrate [<name>] [--repeat <k>] [--json]Decides every record once more, each distinct input once or k times, and reports what the confidence meant: the whole call right by bins with their counts and intervals, the wrong calls at or over each bar per thousand, the spread over repeats, the log's lines apart. --json prints one object. Exits 1 on a call wrong at or over its bar, or a bin of a hundred calls over-confident: Calibrating

None of these needs the classifier's key but test and calibrate, which ask it, or a recording.

Author

CommandDoes
evoke new <name>Writes ./<name>/reflex.toml, <name>.mts and reflex.d.ts from the template. Refuses a directory that exists
evoke new --playbook <name>Writes ./<name>/reflex.toml alone, a playbook of three steps from the template
evoke checkIn a reflex directory: the manifest's lines to fix, the body found and loaded, lint, reflex.d.ts rewritten when it changed, and the contract diffed against the repository's newest tag

Exit codes

CodeMeaning
0Ran, or the command did what it said
1A body or the machine failed. Also evoke test with a failing case, and evoke calibrate with a call wrong at or over its bar
2Abstained, or declined at a prompt
3Needs a human: a missing key, nothing installed, an untrusted project, a prompt without a terminal, a line to fix
4The adapter failed

Files

Everything evoke reads or writes in a project, and who owns each file.

<project>/
├── evoke.toml            yours          the adapter, the reflexes, their settings
├── evoke.lock            evoke's        written whole by add, update and remove; realised by sync
├── evoke.d.ts            evoke's        written with the lock: Reflexes, for load<Reflexes>
├── overlays/<name>.toml  yours          your wording for one reflex, by local name
└── vocab/<name>.toml     yours          your words, by vocabulary name

<reflex>/
├── reflex.toml           the author's   the manifest
├── <body>.mts            the author's   the file run names; nothing for an argv, or for a playbook, whose body is steps
└── reflex.d.ts           evoke's        written by check: Args, Config, Context, Result, Reflex

evoke edits your files in place, keeping comments and order. It verifies each write by reading it back. On failure it writes nothing, and prints the line. The lock and the two .d.ts files are rendered whole. A file that is a symlink is written through; the link stays.

evoke.toml

adapter = "jev"                    # required; a name resolved against evoke's built-ins, never a path

[reflexes]                         # local name = ref
lights = "radhi/home/lights"       # owner/repo[/dir]: unpinned, update moves it
timer  = "radhi/timer@1.0.1"       # @tag pins it
clock  = "https://git.example.com/clock.git#clock@2.0.0"   # any git host; https or ssh; #dir and @tag optional
hello  = "./hello"                 # a local directory, relative to this file; never locked

[config.lights]                    # per reflex, the keys its [config] declares
bridge = "10.0.0.2"                # plain
token  = { env = "HUE_TOKEN" }     # from a variable; the only form a secret may take

[adapters.jev]                     # per adapter, under its name; inert unless selected
gate = { route = 0.5, fits = 0.3, read = 0.8, write = 0.9, whole = 0.3 }   # each a probability; read ≤ write

The built-in adapters are jev, openjev, clef, clef_flash and replay; [adapters.openjev] takes the same gate table as [adapters.jev]. clef and clef_flash ship no thresholds, so a gate table of theirs gives route, read and write. Local names match [a-z][a-z0-9_]*. The names none, unstated, fits and weave are reserved. A missing evoke.toml at a root means the default project: adapter = "jev", nothing installed.

evoke.lock

lock  = 1
evoke = "0.1.0"

[adapter]
name = "jev"
id   = "jev-1.13.0"

[reflexes.lights]
ref    = "radhi/home/lights"
tag    = "1.2.0"
commit = "7a498d11d5375f3cb65c575c3186bc64ecac7f52"
h1     = "h1:a30b1bbcf52390c4b1311685ceee9f886b81b7699cde1374040979ad00c25d3a"
effect = "write"

Per remote reflex: the ref without its pin, the tag, the commit, the content hash of the reflex directory, and the effect you consented to. Local reflexes are not locked. h1 is a SHA-256 over sha256sum's own lines for every file in the directory. Files are taken in byte order of path, and the project's five names are skipped:

$ find . -type f | sed 's|^\./||' | LC_ALL=C sort | xargs sha256sum | sha256sum

overlays/<name>.toml

description = "…"                                   # replaces whole
not_for     = ["…"]                                 # replaces whole
tags        = ["…"]                                 # replaces whole
effect      = "destructive"                         # may only tighten
confirm     = "…"                                   # placeholders name required arguments

[args.state]                                        # by its current name, or a former one declared with was
ask = "…"
options.dim = "…"                                    # new wording for an existing key only

[examples]                                          # sent; may assert vocab arguments
"kill the lights" = { state = "off" }

[tests]                                             # held out; a shipped example named here stops being sent
"light a candle" = false

A contract key in an overlay makes the reflex inactive: run, config, yields, and an argument's source, range, flag or optional. A key that addresses nothing the reflex has, an argument or an option key of its own, is orphaned: skipped, and reported by update.

vocab/<name>.toml

den    = "The TV room downstairs; also 'the snug'."
office = { what = "The upstairs study.", value = "group-7" }

A word is one clean line, trimmed, spaces allowed, and unique under identity. none and unstated are reserved. what is what the classifier reads. value is what the body receives instead of the word.

evoke.d.ts

// Generated by evoke add, update and remove; do not edit.
// What a decision's args carry, per installed reflex: load<Reflexes> narrows them by reflex.

export type Option<K extends string> = { type: "option"; key: K }
export type Word = { type: "word"; word: string; value?: string }
export type Pick<T extends string, V> = { type: "pick"; span: { start: number; end: number; text: string }; value: { type: T; value: V } }
export type Day = { type: "offset"; days: number } | { type: "weekday"; weekday: "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday"; which?: "next" | "this" | "last" } | { type: "day"; day: number } | { type: "calendar"; year?: number; month: number; day: number }
export type Flag = { type: "flag" }

export interface Reflexes {
  timer: {
    /** How long? */
    duration: Pick<"duration", number>
    /** What is the timer for? */
    label?: Pick<"quoted", string>
  }
}

A date is Pick<"date", Day>, a time Pick<"time", string>, an amount Pick<"amount", { amount: number; currency: string }>, a code Pick<"code", string>. A reflex whose manifest does not read is left out. The file imports nothing.

reflex.d.ts

// Generated by evoke check; do not edit.

export interface Args {
  /** How long? */
  duration: number
  /** What is the timer for? */
  label?: string
}

export interface Config {}

/** What the body receives beside its arguments. */
export interface Context {
  /** The person's own words, or the step's words in a plan. Empty when `evoke run` calls it by name. */
  input: string
  config: Config
  /** Aborted at the deadline, on Ctrl-C or SIGTERM, when an app cancels the run, and when evoke ends: stop. */
  signal: AbortSignal
}

/** What the body returns: the text a person reads, and data an app may use. */
export type Result = string | { text: string; data?: unknown }

/** The body: the default export of the file `run` names. */
export type Reflex = (args: Args, context: Context) => Result | Promise<Result>

Written for a file body only, and only when it changed.

A plan file

evoke try --save <file> writes one; evoke run <file> runs it. It is yours, wherever you put it; <name>.plan.json is the recommended name. Written whole as JSON, its second line says what it holds:

{
  "note": "Written by evoke try --save: the sentence and the classifier's answers, in clear. evoke run runs it exactly, or says which pin moved.",
  "plan": 1,
  "input": "pull september's bank transactions, invoices, card expenses and payroll, reconcile them, post the closing entries to the ledger, then send the report to cfo@example.com",
  "weave": { "input": "…", "steps": [ { "n": 1, "text": "pull september's bank transactions", "decision": { "call": "bank month=\"september\"", … } }, … ], … },
  "evoke": "0.10.1",
  "adapter": { "name": "jev", "id": "jev-1.13.0" },
  "gate": { "route": 0.5, "fits": 0.3, "read": 0.8, "write": 0.9, "whole": 0.3 },
  "set": "h1:…",
  "reflexes": { "bank": { "item": "h1:…", "path": "./bank" }, "ledger": { "item": "h1:…", "ref": "acme/books/ledger", "tag": "1.2.0", "h1": "h1:…" }, … },
  "vocab": { "months": "h1:…" },
  "answers": [ { "text": "pull september's …", "raw": { "weave.split_0": { "yes": 0.9 }, … } }, … ]
}

The file holds, in clear, the sentence, the words it used, and every number the classifier answered; it holds no setting's value, no variable and no key, and no time and no name. A remote reflex is pinned by the lock's h1; a local reflex's body is not pinned, as it is not locked. grep '"call"' lists the calls.

Machine-local files

These live outside every project, under XDG paths. The store, the answer cache and the test baselines are in ~/.cache/evoke/. The log, trust, the runtime path and the REPL's history are in ~/.local/state/evoke/. Environment lists each.

The JSON line

--json prints one line per input. The line holds the input, the decision's fields flattened next to it, the adapter calls made, and the result or the error when a body ran. It is the filter format. The log holds the same line, with the adapter's raw answers and the input's candidates next to it. why reads that. The SDK's Decision is the same object, plus values and plan.

Shape by outcome

FieldrunconfirmaskabstainHolds
input●●●●The sentence as decided
outcome●●●●"run", "confirm", "ask", "abstain"
reflex●●●The winner's local name
args●●●Per argument, a typed value; partial for an ask. A pick read from words that spell it out carries typed, the value as it is typed
basis○○○Per value read from the input, what it stands on, by by: ask, its own question; views, two questions that agree; view, one of them and the words that hold it; either carries anchored where those words, asked alone, name the value; words, the words and a yes; spelled, words that spell it out and a yes, with shape where the value was held against the argument's examples: whole, completed by a case or a separator the examples share, or open; only, the one candidate of its kind and a yes; text, a text typed without quotes, with the others it could have been; shared, a value another part of the sentence states, from its words, and a yes
call●●The call on one line: lights room="den" state="off"
effect●●"read", "write", "destructive"
confidence●●●The weakest judgment's probability
weakest●●●{ question, top, p }
judgments●●●●Every choice read: { question, top, p }
contenders●●●●The ranking: { reflex, route }
runner_up○○○The second reflex, when there is one
prompt●{ own, reason, template }: evoke's line, why the call waits in one line, and the reflex's question
left○○○The words of the input that no value holds, each with what it does: { words, does, p }, does one of action, answers with its arg, result, nothing, more; cut is true where the words follow a value and may be part of it, again where they say the call is wanted once more, read where a value of the call was read from them
whole○○○How far the call holds all the input says, where the call was read back against it
quotes○○○The texts in quotes that no value took, each with its marks, where the reflex takes a text in quotes
because●Why it stopped, in order: destructive, no_gate, under_floor, one_view, respelt, text_read, more, cut, quoted, text_left, whole, detail, named, pointed
unconsumed○○●Typed spans no argument took
missing●Per missing argument: arg, ask, because (unstated, out_of_range, not_offered, unsettled or unread), words when the input holds words that answer the ask, choices; a pick's choices.recent lists the values recalled from this session's results, when any, and its choices.readings what the words that answer the ask read as, when they say a value aloud; likely, the listed word made ready for a yes, when there is one
trace●●●●One entry per adapter call, a round of questions each: { adapter, questions, ms }. Empty when the answers came from the cache
result○{ text, data? } when the body ran
contained○Whenever the body ran, with result or error: whether this machine held it to its declaration, { "type": "full" }, or "partial" or "none" with why, one sentence
error○The failure's message when it did not. The line is all that prints; the exit is 1
frames○With error, the frames of the error a JavaScript body threw, from its first: at body (throws.mts:1:1)
cancelled○true when Ctrl-C ended the body; a step of a weave says it as status and why instead

A sentence read as several steps (Weaving) prints one line per step, as each runs, the line of its decision with these fields more: step and steps first, the step's number and the count; bound after trace, where a value came from another step, [{ arg, from, field, value }], value absent for a whole result, which stays on its source's line as result.data; shared, where the sentence said a word once for several steps and this one took it, { "<arg>": { "word", "via" } }, via fill for a required argument and rewrite for an optional one written into the step's words; beside, where a day of the month was read beside the day of a step this one takes from, { "<arg>": <step> }; from, where a playbook wrote the step, [{ playbook, step, slots }], outermost first; when, where the step runs only under a value an earlier step yields, { step, field, is }; and status last, with why when the step stopped. A plan a playbook wrote prints the sentence's own line first: step 0, steps the count, the decision that picked the playbook, playbook and slots, and no status, so a filter counting lines per input sees one more. A request that is only what not to do prints one abstain line over the whole input, with no judgments and no contenders, and because [{ "type": "nothing_to_do" }]; a request refused before its plan was made prints the same line with the reason, conditional with its text. A plan stopped before any step ran prints every step's line at once, its status refused, skipped, declined or unanswered. A step run from a plan file carries pinned too, { "file", "id" }: the file as shown, ~/month.plan.json, and the SHA-256 of what was read, h1:…. evoke try --json prints the plan whole instead, on one line: input, steps with each step's decision, binds, stages, verdict, exclusive, excluded, splits, count, and trace, every adapter call the plan took.

FieldHolds
status"ran", "failed", "declined", "refused", "skipped", "unanswered"
why{ "type": "earlier_step" }, { "type": "nothing_to_take", "from" } and { "type": "too_large", "from" } with the source step, found_nothing, { "type": "not_chosen", "from", "value" } for a step another value picked past, no_reflex, cancelled, { "type": "read_as", "reflex" }, or { "type": "said", "message" }: a prompt's own line, a failure, a question no one answered

In the plan evoke try --json prints, a step's refs count the steps they may name from 0; step, after, stages and binds count from 1. A step carries shared and beside as the line does, typed where the plan wrote its words anew, the words as typed, and repair where it was not one part decided on its own: narrowed or spliced, an item of its neighbour's task; merged, a part read with its neighbour; split, a part of a joint the classifier read as one thing; corrected, a part that says what not to do read with its step; named, a part that matched nothing alone, read as the reflex the whole sentence gives its words, its decision judging the reflex by that question, weave.span_<start>_<end>, in place of route. spliced is also a part put in the words of the step before it: one that holds only values, or one that says the same for another value. So is each step written for one of the things that words like both point at. Its decision's because holds pointed, { "type": "pointed", "arg", "words" }: the argument, and those words as typed. first and verb are two verbs before one thing: the first read without the second, and the second a step of its own that takes from it. apart is a step read for the one value its call holds, with its words as typed in typed. again is the step before it written again for another value its words name, with that value in place. Its decision judges the reflex by the question whether the request asks for that value as well, weave.again_<start>_<end>_<reflex>__<argument>, in place of route. The answers are also where it does, instead where it means the value in place of the one read, out where it rules the value out, and nothing where the words say something else. A binding's via is fill, rewrite or takes, a whole result by the name its source returns, with no kind; the verdict's because names no_source and several_sources where such a result stops the plan. Each of splits carries cut where the plan cuts the sentence there; count, where the sentence could be cut, holds one, how surely it asks one thing, and as_one where it stands as one step for it. A plan carries folded, [{ text, into, gave, picked }], the parts of the sentence folded into a step — a call typed again, or a step a playbook wrote — with the arguments the part gave and the word of it that picked the step among several; and asides, [{ text, remark, does, by }], the parts that ask for nothing: a remark set aside, or words that may add a detail to a step, does the share of each answer, { aside, detail, asks }, and by where the part was set aside by its words alone, own for your own action or courtesy. A plan a playbook wrote carries from on each of its steps and when on a step that may not run; its verdict's because holds reviewed per playbook, with the step, the sentence's text and its prompt, and names what refused it: nested, too_deep, too_long, conditional, excluded, or, for a branch, no_field, bad_value, maybe_source or branch_into_plan.

A call by name, evoke run --json, prints the same line with nothing judged: input is empty, there is no confidence, weakest, judgments or contenders, trace is empty, and result or error says what the body did. A destructive call carries prompt and because like any confirm. Nothing was decided.

The plan file

evoke try --save <file> writes the plan whole as JSON, readable without a tool (Files): note, fixed text; plan, 1; input, the sentence as typed; tags, when any; weave, the plan as try --json prints it less trace; evoke, the version that wrote it; adapter, the lock's { name, id }; gate, the adapter's floors, absent when it declares none; set, the plan digest; reflexes, per active reflex item, the hash of its wording, consent and settings, with path for a local one, or ref, tag and h1 for a remote one, or nothing more for one handed to the SDK as code; vocab, each vocabulary's hash; answers, every classifier answer the plan took, { text, raw } each, the whole sentence's own questions first. run <file> refuses the file when any of these differs here, with the line and the fix.

Values

{ "type": "option", "key": "off" }
{ "type": "word", "word": "office", "value": "group-7" }
{ "type": "pick", "span": { "start": 18, "end": 28, "text": "10 minutes" }, "value": { "type": "duration", "value": 600 } }
{ "type": "flag" }

A pick's value is what a recognizer read: the number, the seconds, the text; a date's reading, { "type": "offset", "days": 1 }, { "type": "weekday", "weekday": "friday", "which": "next" }, { "type": "day", "day": 14 } or { "type": "calendar", "year": 2027, "month": 5, "day": 4 }, resolved to a day only when the body runs; a time as "17:30"; an amount as { "amount": 1200, "currency": "EUR" }; a code as its text. Its span is where it was read, in character offsets of the input.

Examples

A run, from evoke try --json:

{"input":"kill the lights in the den","outcome":"run","reflex":"lights","args":{"room":{"type":"word","word":"den"},"state":{"type":"option","key":"off"}},"call":"lights room=\"den\" state=\"off\"","effect":"write","basis":{"room":{"by":"views","ask":0.92,"reader":0.92},"state":{"by":"views","ask":0.94,"reader":0.94,"yes":0.95}},"left":[{"words":{"start":0,"end":15,"text":"kill the lights"},"does":"action","p":0.9}],"whole":0.9,"confidence":0.92,"weakest":{"question":"lights.room","top":"den","p":0.92},"judgments":[{"question":"route","top":"lights","p":0.95},{"question":"lights.room","top":"den","p":0.92},{"question":"lights.state","top":"off","p":0.94}],"runner_up":{"reflex":"timer","route":0.01},"contenders":[{"reflex":"lights","route":0.95},{"reflex":"timer","route":0.01},{"reflex":"volume","route":0.01}],"trace":[]}

An ask, from a filter with no terminal. The line stands for the prompt that could not be shown, exit 3:

{"input":"kill the lights","outcome":"ask","reflex":"lights","args":{"state":{"type":"option","key":"off"}},"basis":{"state":{"by":"views","ask":0.74,"reader":0.74,"yes":0.95}},"left":[{"words":{"start":0,"end":15,"text":"kill the lights"},"does":"action","p":0.9}],"unconsumed":[],"confidence":0.74,"weakest":{"question":"lights.state","top":"off","p":0.74},"judgments":[{"question":"route","top":"lights","p":0.95},{"question":"lights.state","top":"off","p":0.74}],"runner_up":{"reflex":"timer","route":0.01},"contenders":[{"reflex":"lights","route":0.95},{"reflex":"timer","route":0.01},{"reflex":"volume","route":0.01},{"reflex":"power","route":0.0}],"missing":[{"arg":"room","ask":"Which room?","because":{"type":"unstated"},"choices":{"type":"vocab","words":{"den":"The TV room downstairs; also 'the snug'.","office":"The upstairs study."}}}],"trace":[]}

Questions

A question id is route, fits.<reflex>, <reflex>.<argument>, or weave.<name>: a question evoke asks on its own account, beside a reflex's, and no reflex is named weave. A choice's keys are option keys, vocabulary words, <start>-<end> for a pick's candidates, yes and no for a flag, local names for the route, and the sentinels none and unstated. On an argument's question, none says the input states a value that is not among the choices.

evoke calibrate --json

One object, the report of Calibrating: adapter; records, reflexes, inputs, repeats; outcomes with run, confirm, ask, abstain; bins, each { lo, hi, calls, right, interval, claimed, thin, over_confident }, a bin with no call left out; unknown; abstained as { count, right, interval }; bars with read and write where the effect has a complete call, each { bar, wrong, calls, per_thousand, at_most, near } and near a list of { at, run, wrong }, and whole where a call over its bar was read back against the input, { bar, calls, run, wrong, per_thousand, at_most, held_right }; questions, each { kind, judgments, right, interval, claimed } with kind one of route, options, vocab, pick, flag; brier with brier, reliability, resolution, uncertainty, absent without a call; misses, each { case, outcome, reflex?, confidence?, mismatch, wrong }; under --repeat, variance with flips, verdict_flips, spread as { median, p90, max }, straddling, wrong_at_bar, and moved, each { utterance, confidence?, route, winners, outcomes, wrong }; and log with adapter, decisions, ran, confirmed_ran, confirmed_stopped, asked, abstained, failed, skipped, stopped_by_confidence as [{ lo, hi, count }], records as { count, right, interval }, and unread. An interval is [low, high]; a rate is per thousand.

Rules of the wire

Keys are snake_case. An absent optional is omitted, never null. A tagged value carries type. A decision carries outcome. Numbers are numbers: 0.85 and 0.850 are one value.

Environment

The variables evoke reads, and the paths it writes under. Nothing is read from a file it does not own, and nothing is sent anywhere but the adapter you chose.

Variables evoke reads

VariableRead byMeans
TYPESAFE_API_KEYThe jev adapter, when decidingThe classifier's key, from typesafe.ai. Never written to a file
OPENJEV_API_KEYThe openjev adapter, when decidingThe classifier's key, from openjev.sh. Never written to a file
CLOUDFLARE_API_TOKENThe clef and clef_flash adapters, when decidingThe classifier's token, from dash.cloudflare.com. Never written to a file
CLOUDFLARE_ACCOUNT_IDThe clef and clef_flash adapters, when decidingThe account the token belongs to: 32 hex digits in lowercase, which go into the address. Anything else is refused
EVOKE_ANSWERSThe replay adapterA recording to answer from, with adapter = "replay" in evoke.toml
EVOKE_PLATFORMEvery decision, add, show, testlinux or macos: the platform a manifest's platforms is judged by, this machine's when unset. Set it to decide or test as another machine would; a body still runs here
EVOKE_TODAYA body's run, checkYYYY-MM-DD: the day a date argument resolves against, your machine's local day when unset. A value that is no day stops the run
<VAR> of a --env settingA body's runA config value, most often a secret, resolved for that run only
NO_COLORThe terminalSet and not empty: no colour. The spinner and the line editor stay
TERMThe terminalUnset, empty or dumb: no colour, no spinner, no line editor
HOME, XDG_CONFIG_HOME, XDG_STATE_HOME, XDG_CACHE_HOMEPathsWhere the project and the machine-local state live
PATHadd, sync, every runWhere node is found and recorded, and where a declared program or an argv's program is found at each run; git too
GIT_*FetchingYour git configuration and credentials apply; GIT_TERMINAL_PROMPT=0 is set
HTTPS_PROXY, NO_PROXYThe classifier's connectionA proxy the connection goes through, in the CLI and the SDK, and the hosts that bypass it; https_proxy and no_proxy win over the upper-case names. An address that is not http or https is refused, never bypassed. The endpoint itself never moves

Paths

PathHoldsWritten by
$XDG_CONFIG_HOME/evoke/ · ~/.config/evoke/The home projectyou, add, teach, …
$XDG_CACHE_HOME/evoke/store/<h1>/Fetched reflex directories, by content hashadd, sync
$XDG_CACHE_HOME/evoke/answers/<plan>/The adapter's answers, per installed set, utterance and questions askedevery decision, try
$XDG_CACHE_HOME/evoke/baselines/<plan>.jsonevoke test's last verdicts per installed settest
$XDG_CACHE_HOME/evoke/runtime.jsonWhether the node a file body runs under can keep it off the network, as Node 25 and later can; asked once per node binarya file body's run, check
$XDG_STATE_HOME/evoke/log.jsonlOne JSON line per decision; why reads the lastevery decision
$XDG_STATE_HOME/evoke/trust.tomlTrusted project roots and their content digeststrust, every write
$XDG_STATE_HOME/evoke/runtimeThe real path of node a file body runs underadd, sync
$XDG_STATE_HOME/evoke/historyThe REPL's lines, the last 1 000the REPL
$TMPDIR/evoke-…/A body's private temporary folder, for one runevery run; removed after it

$XDG_STATE_HOME defaults to ~/.local/state, and $XDG_CACHE_HOME to ~/.cache. Everything under cache can be deleted. sync and the next decision rebuild it. No key and no config value is ever written under either. The log and the history hold what you typed and what a body returned. Only you can read them.

What a body sees

A file body runs under a scrubbed environment of five variables: PATH, HOME, TMPDIR, LANG, TERM, with TMPDIR a private folder made for the run. It receives its arguments, the input, its config and a signal through the call, and touches what its manifest declares under [needs]: The body. An argv body gets the same five, plus:

VariableHolds
EVOKE_CONFIG_<KEY>Each [config] key, upper-cased, as its value; a plain one under the home expanded
EVOKE_INPUTThe person's own words; the step's words when a plan runs the reflex; empty under evoke run

Secrets reach a body only this way, for the length of one run.

Time

One decision has 30 seconds, shared by the adapter's answer and the body's run. A prompt never counts. The jev adapter gives a request 1.5 seconds once connected, and openjev 3 seconds, since OpenJEV forwards the request onward. clef gives it 6 seconds and clef_flash 3. Each retries once after a connect error or a server error, and waits out a 429 that says how long to wait, within the deadline.

Diagnostics

Every line that needs something from you has one shape: what is wrong, then the literal command that fixes it. It exits 3:

  lights: vocabulary "rooms" is empty  →  evoke vocab rooms add <word> "<meaning>"
  ~/app is not trusted                  →  evoke trust

Under --json, the decision line stands for a prompt that could not be shown. In the SDK, the same lines are the message of a DiagnosticError. Each problem carries its fix and command.

The fixes

The set of fixing commands is closed. Each line ends in one of these:

FixThe problem it answers
export <VAR>=<value>A key or a --env setting's variable is not set
evoke add evoke-build/reflexesNothing is installed
evoke add <ref> --as <name>A local name is taken, or a [reflexes] line names something not locked
evoke vocab <name> add <word> "<meaning>"A vocabulary is empty, or a word is not in it
evoke vocab <name> add <word> "<meaning>" --value <path>A word's value is not the path a declaration takes it for
evoke vocab <name> remove <word>A vocabulary offers more words than the classifier takes in one question
evoke vocab <name>A word to remove is not in the vocabulary
evoke add <ref>…The rest of a collection one taken name refused; a ref without a tag it has not, or without --as
evoke config <reflex> <key> <value>A declared setting is not set
evoke config <reflex> <key> --env <VAR>A secret is not set, or was set plain
evoke show [<reflex>]Something answered by looking: an unknown argument, an option not offered, an undeclared key, a reflex not installed
evoke teach "<phrase>" not <reflex>A newcomer steals a phrase an installed reflex claims, or fits it over the floor
evoke update [<reflex>]A name is already locked, a tag moved or vanished, or a retired adapter
evoke update --accept <reflex>An effect upstream loosened, or a declaration it widened, since you consented
evoke syncA reflex is not in the store, or no runtime is recorded
evoke trustThe project is not trusted, or changed since it was
evoke remove <reflex>A local reflex's directory or manifest is missing; a fetched reflex reached past its declaration
evoke checkA body does not exist, does not load, or exports no function
evoke new <name>No reflex.toml here, or the directory already exists
evoke testThe theft test at add did not finish; the install stands
mkdir -p <path>A folder a body's declaration writes into is not there
evoke --helpThe arguments spell no command
<file>:<line>:<column>A line of an owned file to edit: the schema, a contract key in an overlay, a loosening effect, a local reflex's declaration to widen, or one naming a path that is not there
the command you ranTry again once the reason on the line is addressed: a prompt with no terminal, a value out of range

A body refused past its declaration, or a declared path the machine lacks, is a failure, exit 1, whose line still ends in its fix: the manifest's line for a local reflex, evoke remove <reflex> or evoke update --accept <reflex> for a fetched one, the value's source for a {name}, mkdir -p for a folder the body writes into.

What lint reports

At evoke check and evoke add, lint reports and never refuses. Each line starts with lint, names the reflex and says what to change. The text is yours to weigh:

LineMeans
the summary is 120 characters; the cap is 100A size passed its cap: the summary, the description, not_for, the options, the records, a sentence or a step
description addresses the model: "you must"A phrase speaks to a model instead of describing the action
effect is absent, which means destructive; write itThe manifest leaves effect out, and a reader cannot see what that means
confirm reads no value back; name {release}The reflex has a required argument, and the confirm names none
confirm asks "Are you sure"; say what the call will doThe confirm asks for a yes without saying what it covers
args.state.options.on repeats the ask; a meaning answers itAn option's meaning is the question again
args.label reads text in quotes, and no example shows themA quoted argument, and no example holds a quotation mark
args.level has no record that leaves it out; assert level = false onceNo example or test asserts the option or the pick unstated
examples holds 2 requests; write three at leastFewer than three examples state a request of the reflex
step 4 holds "then"; one step is one actionA step holds a connective or a comma
step 6 states "#incident"; a word another team would change is a slotA step writes in a channel or an address
step 3 says "check that writes"; a step refers by "that <noun>": say "check whether"A step opens with check that
step 1's words are the summary's or an example's; word a step apart from the situationEvery word of a step that says something is in the playbook's own summary or examples

evoke add prints two lines more for a playbook, once its steps are decided over the set it joins:

LineMeans
claims write; its steps reach destructiveA step reaches a reflex with a tighter effect than the playbook claims
step 3 reaches logs, which lacks the tag outageA request narrowed with --tag outage would leave that step without its reflex

Why a reflex is inactive

An inactive reflex is left out of every decision, and show lists why, one line each:

LineMeans
runs on macOS onlyThe manifest's platforms leave this machine out
vocabulary "rooms" is emptyAn argument names a vocabulary with no words
config "bridge" is not setA declared setting has no value
config "token" is a secret; it is set from a variableA secret was given plain
HUE_TOKEN is not setA setting names a variable the environment lacks
an overlay line, with its file and positionA contract key, a loosening effect, or a parse error in your overlay
a manifest lineThe shipped manifest does not read

A missing store entry or runtime is not inactive but a stop. The fix is evoke sync. An effect upstream loosened is not inactive either. The reflex runs under the effect you consented to, until you accept.

Failures and faults

A body that throws, exits non-zero, or overruns the deadline is a failure. Its message prints, exit 1. A thrown error's frames are kept for its author: evoke why shows them under the decision, and --json carries them as frames. An adapter that cannot be reached, returns an error status, or answers something the core cannot validate is a fault, exit 4. A key the classifier refuses is one, and its line ends in the export that replaces the key. The core fails closed rather than guess. Both name what to do next. A path under your home prints as ~/….

Glossary

Every term evoke uses, one line each: the outcomes of a decision, the parts of a reflex, the files of a project, the adapter, and the words of a weave.

TermMeaning
AbstainThe outcome when none wins the route, or the winner is under the route floor. Exit 2
AccountWhat each run of the input's words that no value holds does: says what to do, answers an argument's ask, says what is wanted from the result, asks for nothing, or asks for another thing. why names the words that answer an ask, those a call leaves out and those about the result; the JSON line carries them as left
AdapterThe classifier behind a decision, as an object answering typed questions with probabilities. jev and openjev reach Jev, through TypeSafe AI's API or through OpenJEV; clef and clef_flash reach Clef on Cloudflare's Workers AI; replay answers from a recording
ArgumentA question about the input and a value for the body: options, vocab, pick or flag
AskThe outcome when a required argument is missing: evoke asks the argument's own question
BindingA value of one step's result taken by a later step, by kind: filled into a required argument, or written into the words for an optional one; or the whole result, by the name its source returns, handed beside the decision
BodyWhat run names: a .mts/.mjs file exporting a function, or a program with its arguments; a playbook has steps instead
BranchSteps of a playbook that run only under a value an earlier step yields, { say = "…", when = { landing = "yes" } }, listed right after that step; the plan prints every one, the result picks, the rest are skipped clean
CallA reflex with its arguments filled, on one line: lights room="den" state="off"
CapA reason a complete call stops at confirm: destructive, no gate, under the floor, a value read from your words another way, words that ask for more, a call that holds less than you typed
ChangeA reflex whose effect is write or destructive: it changes something, where a lookup only reads
CollectionA repository of reflex directories
ConfidenceThe lowest probability among the route and every value the call holds
ConfirmThe outcome when a call is complete but capped: [y]es [n]o [t]each
ContractWhat a user cannot override: run or steps, argument names and sources, option keys, ranges, config keys, what a reflex yields, returns and takes
EffectWhat running a reflex does: read, write or destructive. Absent means destructive, so a manifest always writes it
Effective manifestThe shipped manifest with your overlay merged in; what show <name> prints
FitsThe yes/no question asked when a reflex is added: does it do what another reflex's example asks? At or above its floor, add names the example
FlagA yes/no argument. The body receives true or nothing
FloorThe bar a decision must clear. A threshold the adapter ships, meaning the probability this is right: route, read, write, whole, and fits at add
FoldA part of a sentence that repeats a step a playbook wrote: removed from the plan and named under it, folded "…" into <n>
GateThe step that turns confidence and effect into an outcome
h1The content hash of a reflex directory, h1:<sha256>, in the lock and the store
Home project~/.config/evoke/: the project used when no evoke.toml is found upward
HubWhere reflexes are found; today the collection evoke-build/reflexes
IdentityHow utterances are keyed: NFC, lower-cased, whitespace collapsed, trailing punctuation dropped
InactiveA reflex left out of every decision until a problem is fixed: an empty vocabulary, an unset setting, an overlay that does not read
Local nameThe key under [reflexes]: what the classifier reads and what the overlay file is named after
Lockevoke.lock: per remote reflex, the ref, tag, commit, h1 and consented effect. Also the adapter
LookupA reflex whose effect is read: it finds or shows something, and changes nothing
Manifestreflex.toml: what the classifier reads and what the body receives
Near neighbourWhat a person might type that sounds like a reflex's request and is not: named under not_for, and written as a false test
Overlayoverlays/<name>.toml: your wording for one reflex, merged over the shipped one
PickAn argument read from an exact span of the input: number, duration, email, url, quoted, date, time, amount, code
PinA question evoke asks the classifier on its own account beside a reflex's own, keyed weave.<name> in a recording: a second view of a listed word, a yes on a word of the input, what a run of words does, whether a call holds all the input says. Also what a plan file is checked against before it runs: the evoke version, the adapter's id and gate, the set's digest, each reflex and vocabulary by hash, a remote reflex's h1
PlanThe installed set compiled to questions. Its digest keys the cache, the baselines and every decision
Plan fileA weave saved by try --save: the sentence, the classifier's answers and the pins it was decided under; run <file> runs it exactly, or names the pin that moved
PlaybookA reflex whose body is a plan: steps, one sentence per step with {slots}, reached by a short sentence and decided step by step over the installed set
ProjectA directory of files you own: evoke.toml, evoke.lock, overlays/, vocab/
RecalledA value an earlier body of this session returned under the field a pick's recent names, listed at the ask and chosen by you
RecordOne line of [examples] or [tests]: `"utterance" = { assertions }
RecordingAn answers.toml: an adapter's declaration and its answers by utterance identity
RefWhere a reflex comes from: owner/repo[/dir][@tag], a git URL, or ./dir
ReflexA directory: a manifest and the file or program it names. Plural: reflexes
ReturnsThe name a body's whole data goes by, returns = "deploys", for a later step of one sentence to take
RouteThe one choice over every installed reflex plus none
RunThe outcome when a call clears its floor. Also the key naming the body
RuntimeWhat runs a body: Node for a file, nothing for an argv
SituationWhat a person faces, typed in one sentence, a problem or a routine: checkout is failing in eu-west, close the month. A playbook is reached by its situation
SlotAn argument named in a step of a playbook, {service}: filled from the sentence or asked before the plan prints; in square brackets, written only when stated
SpanAn exact piece of the input, by character offsets
StepOne part of a weave, decided as one input is, a playbook's sentences among them; numbered as the plan prints it, run at its turn
Store~/.cache/evoke/store/<h1>/: fetched code, re-hashed before every run
TagA word under tags for --tag to narrow by. Also a git version tag
TakesAn argument an earlier step's whole result fills by that name, takes = "deploys": never asked, never stated, the plan alone hands it
TrustA project's four owned paths bound to their content. Required outside home
UnstatedThe answer that an argument was not given in the input
UtteranceA sentence a record is keyed by
ViewOne of two questions about a listed word, the argument's own and a second one in other words; a value both give stands, and why says asked a second way. Where they differ, the input's own words decide, or the call waits for a yes
Vocabularyvocab/<name>.toml: your closed list of words, shared by every argument that names it
WeaveA sentence read as several steps: each decided on its own, ordered by the words, a result threaded into a later step, the plan shown before anything runs
WordingWhat a user may override: descriptions, questions, option meanings, records
YieldA field of a result's data a later step may take, declared under [yields] with the kind that reads it

The collection

evoke-build/reflexes is the first-party collection: fifteen reflexes for what a Mac does at a word. Each reflex is one directory, with reflex.toml and the file it runs, where it runs one. There is nothing to build. Thirteen of the fifteen run a file and need Node 24 or newer on your PATH; open and trash run a program the Mac has. Every one follows the rules, so each is a manifest to copy from.

$ evoke add evoke-build/reflexes
ReflexDoesEffectReads from your wordsYieldsYours to set
awakeKeeps the laptop awake for a duration, or until pkill caffeinatewriteduration, a duration, optional
downloadSaves a URL's file to ~/Downloads, or to a place you namewriteurl, a URL; to, one of your places, optionalpathplaces
lockLocks the screenwrite
mailStarts an email in your mail appwriteto, an address; subject, text in quotes, optional
noteAppends a dated line to your notes filewritetext, text in quotesfile
openOpens one of your foldersreadplace, one of your placesplaces
powerRestarts or shuts downdestructiveaction, restart or shutdown
screenshotCaptures the screen, a window or a selectionwritearea, screen, window or selection, optional; clipboard, a flagpath
sleepPuts the laptop to sleepwrite
soundMutes, unmutes, or turns the volume up or downwritestate, off, on, up or down; by, a number of points, optional
timerCounts down, then ringswriteduration, a duration; label, text in quotes, optional
trashEmpties the trashdestructive
visitOpens one of your sites, in a private window on requestreadsite, one of your sites; incognito, a flagsites
volumeSets the output volumewritelevel, a number from 0 to 100
wifiTurns Wi-Fi on or offwritestate, on or off

Yours to set

A reflex that needs your words or a setting for a required argument stays inactive until it has them. evoke says which line gives it what it needs. download runs without places; with it, a place you name. Two vocabularies and one setting cover the collection:

$ evoke vocab places add desktop "The desktop." --value "~/Desktop"      # a word per folder; the value its path
$ evoke vocab sites add github "GitHub." --value https://github.com             # a word per site; the value its URL
$ evoke config note file "~/notes.txt"                                          # a path under your home, or absolute; a file that exists

What they pass on

A reflex may declare what its result holds, for a later step of one sentence to take (Weaving). download and screenshot yield path, the file they saved to, a quoted text: a later step with a quoted argument your words left empty can take it, as note does in download https://evoke.build/llms.txt and note it. No reflex in the collection returns a whole result under a name yet, and none takes one.

What they say back

One lowercase line that says what happened: volume 40% · muted · locked · sleeping · saved ~/Desktop/Screenshot 2026-09-20 at 10.31.05.png · copied to the clipboard · eggs: 3 minutes, rings at 10:34 AM · awake for 2 hours · noted "buy milk" in ~/notes.txt · new mail to ana@example.com about "friday" · opened https://github.com in a private window · saved report.pdf to ~/Downloads (1.2 MB).

How they are built

  • macOS, one self-contained file each: every manifest names it, so on Linux each reflex is inactive, and evoke add says so. open and trash are argv reflexes and need no runtime. Each manifest declares what its body touches under [needs], and evoke add prints it under the row: screenshot writes ~/Desktop and runs screencapture. wifi finds the Wi-Fi device by its port's name: en0 on a laptop, often not on a desktop with Ethernet.
  • What outlives a run detaches and returns at once. awake leaves caffeinate running. timer leaves a script that waits, then notifies with a sound.
  • sleep sleeps with pmset. power sends the login window's own restart and shutdown events. So apps are asked to quit, and no second dialog appears, since the decision was confirmed already. lock opens the system's lock screen and needs no Accessibility permission.
  • download never writes over a file already there. A download the 30 s deadline cuts short is removed, and the run says so. A body that runs a program reports the program's own words when it fails. visit opens a private window through the default browser's own flag, in Chrome, Brave, Vivaldi, Edge or Firefox. Safari opens none from a script.
  • Every reflex writes its effect, carries at least three examples, and has its near neighbours as false tests, in words no example uses. One record leaves out each option and each pick. The reflexes name each other under not_for: timer is not for keeping the laptop awake, and awake is not for timers.

Licence and changes

The collection is MIT, since its scripts are meant to be copied into reflexes of your own. Issues and changes go to evoke-build/evoke. The collection is developed and published from there.

Security

A fetched reflex runs as you, held to what its manifest declares. evoke add and evoke update are the trust decisions. Everything below is what the tool guarantees around them, and what it does not.

What runs, and as whom

  • A body runs with your user, under a scrubbed environment of five variables: PATH, HOME, TMPDIR, LANG, TERM. TMPDIR is a private folder, made for the run and removed after it. The body receives secrets only through its config, for one run. The scrubbing prevents leaks. Read what you install, and its declaration, the next point, with it.
  • A body is held to its declaration. [needs] in the manifest names the paths the body reads and writes, whether it reaches the network, and the programs it runs: What the body touches. Leaving the table out is the tightest declaration: the body's own directory, TMPDIR, and nothing else. Two layers hold the body there. The kernel's: on Linux a Landlock ruleset, no_new_privs and a seccomp filter that closes the network, applied to the process before the body starts; on macOS Seatbelt, through sandbox-exec, with a profile that denies everything the declaration does not name. And, for a file body, Node's permission model, which refuses a read past the declaration with the path before the kernel is asked. A program named in runs runs under the kernel's layer too, so it reaches no further than the body; what it asks the system to do — Finder emptying the trash, the login window restarting — happens outside the declaration, so runs osascript is read beside the effect. evoke check imports a body under the same hold, with nothing configured.
  • A reach past the declaration ends the run. The line names what was reached and the key: ~/secret.txt is not in [needs] reads, example.com is not in [needs] hosts, [needs] runs names no program. Its fix depends on where the declaration is written: the manifest's line for a local reflex; for a fetched one, evoke update --accept when upstream's own declaration already allows it, else evoke remove. The run exits 1, a failure.
  • The status is printed, never assumed. A machine that holds only part of a declaration, or none of it — a Linux kernel without Landlock or before its third version, a Mac without sandbox-exec — still runs the body, and says so: one line at add and at show, not contained <why>; · partly contained or · not contained at the end of the line evoke prints before every body runs; and contained in --json.
  • No code runs at install time. add fetches a tree, reads a manifest, lints it, and writes files. A body runs only at a decision, at evoke run, or when evoke check imports it to see that it exports a function.
  • A reflex handed to the SDK as code is your application's own. It runs in its process, unscrubbed and unheld. It must honour its signal, since nothing can end it from outside. A reflex the SDK loads from a project runs held as far as the package can: under Node's permission model on both systems, and under Seatbelt on macOS. On Linux the kernel's layer needs native code the package does not carry, so a file body runs partly held and an argv body unheld there, and the result's contained says which.

What the classifier cannot do

  • State is only { request }. The adapter sees the sentence and the questions built from installed manifests. Nothing else of yours.
  • Arguments are closed sets, exact spans, or an earlier step's result. A value is an author's option key, one of your vocabulary words, a piece of the input read by a recognizer and checked against its range, or, in a sentence of several steps, a field of an earlier step's result or that step's whole result, bound by the plan and shown on its line before anything runs; or, at a prompt, a value a body of this session returned under a field the author named, listed for you to choose and read by the recognizer. Injected text can choose a call. It can never mint a value. A date reaches a body as a day of your machine's calendar, never as a word.
  • The effect gate covers what it chooses. A destructive reflex always confirms, and there is no --yes. Unattended use is a threshold in a file you own and trust. A read or write reflex over its floor runs without asking. Whatever it does with a span it is handed, like a URL or a quoted text, an injected sentence can make it do. So its effect is the author's promise about exactly that. A write reflex that takes an earlier step's whole result runs with it over its floor, unasked; the plan prints takes on its line first, and a destructive reflex takes none.
  • Prompts cannot be repainted. Control characters and bidi overrides are refused in every manifest, overlay and vocabulary string, and in every span. evoke's own line, with the call, the effect and the weakest judgment, prints before the reflex's template.
  • An input over 2 000 characters is refused, and one decision has 30 seconds.

What a manifest cannot do

  • Wording is linted and conflict-tested at add. Text that addresses a model is named. Every phrase a newcomer would steal from an installed reflex is reported, with its one-line fix.
  • Effect and needs only tighten. The lock holds the effect and the declaration you consented to. Upstream may tighten either at any tag. Loosening waits for evoke update --accept, which says what widened. Your overlay may tighten the effect, never loosen it, and cannot touch [needs].
  • The contract cannot be overridden. An overlay that tries makes the reflex inactive, not looser.
  • Tags are only for --tag. No label, category or claim in a manifest changes what may run.

What the files guarantee

  • Trust binds to content. A project outside home decides only after evoke trust. The digest of its four owned paths is checked at every run. A git pull that changed an overlay is a stop, not a surprise. A local reflex, ./dir, is outside those paths. Its directory is yours, so a pull that changes it runs what it now holds, under the effect its manifest now claims.
  • The lock binds to content. Every remote reflex is pinned by tag, commit and h1. The store's copy is hashed again every time evoke starts. A tag that moved is refused.
  • Fetching is narrow. git is called with a strict ref grammar, --end-of-options, and an allow-list of https and ssh. Symlinks and submodules are rejected. There is no checkout, no hook, and no clone-time script.
  • Secrets are referenced, never stored. evoke.toml names a variable. The value exists in your environment, and reaches a body for the length of one run. evoke writes no key and no config value under the cache or the state directory. The log and the REPL's history hold what you typed and what a body returned. Only you can read them.
  • Adapter names resolve on your machine only, against the tool's built-ins, never from a project directory. So a cloned repository cannot point your decisions at a classifier of its choosing. The endpoint is built into the adapter, whose id keys the cache and the lock, so no file and no variable moves it. The clef and clef_flash adapters put one thing from a variable into their address, your account's id at Cloudflare. It must be 32 hex digits and nothing else, so it picks an account at that one service and never another address. A proxy named in your environment carries the connection there and sees ciphertext: the CLI trusts Mozilla's roots, never your platform's, and the SDK trusts what your Node trusts.
  • The SDK never searches. load takes an explicit root, never fetches, and never writes. It hands each tenant their own vocabulary. So a server cannot be hijacked by whatever project a working directory holds.
  • A plan file holds the sentence and the classifier's numbers, in clear, and no setting's value, variable or key. It can choose among your installed reflexes and your words, never mint a value, and it runs only after your yes over the whole plan and each step's own confirm; a file whose pins moved is refused before anything runs.
  • A playbook reaches only what you installed. Its steps are sentences, decided over your reflexes as typed ones are, under the same gate; it fills no value of its own, and it runs only after your yes over the whole plan, a destructive step confirming again at its turn.

Check it yourself

Nothing on this page needs a key to test. evoke run <call> runs a reflex by name and calls no classifier. The replay adapter answers every decision from a file you write: set adapter = "replay" in evoke.toml and name the file in EVOKE_ANSWERS. Testing shows the file. An answer that tries to mint a value, a key no argument offers or a number that is not a probability, ends the decision with a fault, and nothing runs. The repository's spec/ holds the transcripts and vectors behind the claims above, and mise run test runs them without a key. The contained transcript shows a body refused past its declaration, and a declared path the machine lacks refused before the body runs.

Reporting

Report a vulnerability privately, through GitHub's report form, never in a public issue.