Examples · 02 of 06
One sentence looks up an address and passes it to the next step.
Type one sentence that asks for two things, and the result of the first fills an input of the second. A reflex is a small program you install and ask for in your words. Here one reflex finds Dana's address in your list of people, and the collection's mail reflex starts a draft to it. The lookup is a small reflex you write, shown in part 05. Every session on this page was recorded on a Mac against Jev, TypeSafe AI's model. Two reflexes from the collection do the same with a file's path, on any machine.
The request and the result
01A value passes, a step is declined, a sentence is refused.
evoke splits your sentence into steps, in the order your words give. A classifier reads each step on its own: Jev, TypeSafe AI's model. evoke prints the plan, one line per step, before anything runs. Recorded on 26 September 2026, on a Mac.
- 1«them» points at step 1.
mailneeds an address your words did not give.contact's result holds one, of the kindmailtakes, so the plan printstakes email from 1. Oncecontactruns, the address fillsto, and your mail app opens the draft. - 2A no stops the sentence at that step. Emptying the trash cannot be undone, so evoke asks first, however sure it is. The answer was no, and the lock step after it was skipped.
- 3«feed the cat» matches no reflex you installed. So evoke refuses the whole
sentence before anything runs, the lock included, rather than do half of it. The exit code is
[2], as it is for a no.
contact is the reflex part 05 shows you how to write, over your list of people.
mail, trash and lock are the collection's. The draft opened in the Mac's
mail app, and nothing was sent. How the address passes ↓
How the address passes
02The address passes because both reflexes declare the same kind.
Each reflex has a manifest, its reflex.toml, which says what it does and what it takes.
contact's manifest also declares what its result holds. When that result and
mail's input are of the same kind, evoke plans the pass and shows it before anything runs.
| Where | The line | What it does |
|---|---|---|
contact/reflex.toml | [yields] email = "email" | contact's result holds a field named email, and its value is an email address. |
contact/contact.mts | return { text, data: { email } } | text is the line you read. data.email carries the value that passes. |
mail/reflex.toml | [args.to] pick = "email" | mail's to takes an email address. |
| the plan | 2 mail · takes email from 1 | «them» points at step 1, and email is the kind to takes. So the plan says the address will fill to, and step 2 prints again with it once step 1 has run. |
reflex = 1 description = """ Look up a person's email address in your list of people. One person at a time; prints the address.""" not_for = ["writing to anyone", "phone numbers", "adding a person to the list"] tags = ["people"] effect = "read" confirm = "Look up {name}'s address?" run = "contact.mts" [args.name] ask = "Whose address?" vocab = "people" [yields] email = "email" [examples] "look up dana's address" = {} "what is sam's email" = {} "find the address for dana" = {} [tests] "dana's email please" = {} "email dana" = false
import type { Reflex } from "./reflex.d.ts" export default (async ({ name }) => { // name is the word's value: the address you stored for that person. if (!name.includes("@")) throw new Error(`no address for ${name}`) return { text: `found ${name}`, data: { email: name } } }) satisfies Reflex
dana = { what = "Dana Ortiz, operations.", value = "dana@example.com" } sam = { what = "Sam Lee, sales.", value = "sam@example.com" }
reflex = 1 description = """ Start an email to an address in your mail app. A subject goes in quotes, "like this"; nothing is sent until you press send.""" not_for = [ "checking or searching the inbox", "replying to or forwarding an email", "sending a text or a chat message", "opening your webmail in the browser", "jotting down a note", ] tags = ["mail"] effect = "write" confirm = "Start an email to {to}?" run = "mail.mts" platforms = ["macos"] [needs] runs = ["open"] [args.to] ask = "To which email address?" pick = "email" [args.subject] ask = "What is the subject?" pick = "quoted" optional = true [examples] "write an email to lee@example.com" = { to = "lee@example.com" } 'email priya@example.com about "tuesday lunch"' = { to = "priya@example.com", subject = "tuesday lunch" } 'start a mail to ops@example.com with the subject "server move"' = { to = "ops@example.com", subject = "server move" } 'new mail to bob@example.com with subject "Invoice"' = { to = "bob@example.com", subject = "Invoice" } 'write to sam@example.org, subject "Invoice 42"' = { to = "sam@example.org", subject = "Invoice 42" } [tests] "email jo@example.com" = { to = "jo@example.com", subject = false } 'open a new email to billing@example.com titled "march invoice"' = { to = "billing@example.com", subject = "march invoice" } "email my landlord" = { to = false, subject = false } "any new mail from the bank" = false "text ana that I'm running late" = false "reply to the last email from hr" = false
// Start an email in the default mail app: a `mailto:` URL opened, with the subject when one was given; nothing is // sent. import { execFile } from "node:child_process" import { promisify } from "node:util" import type { Reflex } from "./reflex.d.ts" const exec = promisify(execFile) export default (async ({ to, subject }, { signal }) => { if (process.platform !== "darwin") throw new Error("runs on macOS only") const url = subject === undefined ? `mailto:${to}` : `mailto:${to}?subject=${encodeURIComponent(subject)}` try { await exec("open", [url], { signal }) } catch (error) { if (signal.aborted) throw signal.reason throw new Error(said(error) ?? "the mail app did not open") } return subject === undefined ? `new mail to ${to}` : `new mail to ${to} about "${subject}"` }) satisfies Reflex /** What a failed command said on stderr, when it said anything; else nothing, and the caller's own words stand. */ function said(error: unknown): string | undefined { const text = typeof error === "object" && error !== null && "stderr" in error ? error.stderr : undefined return typeof text === "string" && text.trim() !== "" ? text.trim() : undefined }
test("a pronoun takes the address the step before yielded, into the ask it fills", async () => { const project = await load({ root: home, adapter: replay(answers) }) const woven = await project.weave("look up dana's address and email them") deepStrictEqual(woven.plan.binds, [{ from: 1, to: 2, arg: "to", field: "email", kind: "email", via: "fill" }]) equal(woven.plan.steps[1]?.decision.outcome, "ask") equal(woven.status, "ran") deepStrictEqual(woven.steps[1]?.bound, [{ arg: "to", from: 1, field: "email", value: "dana@example.com" }]) const round = woven.steps[1]?.rounds[0] equal(round?.decision.outcome, "run") if (round?.decision.outcome !== "run") return equal(round.decision.call, 'mail to="dana@example.com"') deepStrictEqual(round.result, { text: "drafted to dana@example.com" }) deepStrictEqual(woven.steps[0]?.rounds[0]?.result, { text: "dana <dana@example.com>", data: { email: "dana@example.com" } }) })
- 1Same kind, so it passes. A result can hold a number, a duration, an email address, a URL or a quoted phrase. A later step takes a field only when your words point back to the earlier step, the input is of the same kind, and your words left it empty.
- 2The lookup reads your list of people.
danaandsamare words in it, and evoke calls such a list a vocabulary. Each word carries the address as its value, which evoke hands to the program, and the program returns it. - 3The SDK test replays the same pass over stand-ins. Its answers come from a file written for the test, so it needs no key and no network, and runs in evoke's own test suite. It checks the pass, the input it fills, the call and both results.
Neither reflex names the other: one line in each manifest makes the pass. When a result passes to the next step →
One boundary
03A reference that takes nothing asks before anything runs.
«for them» points at step 1, but a timer takes no address. evoke says so, and asks whether to run the plan as it stands. Nothing runs until you answer.
$ evoke "look up dana's address and start a 10 minute timer for them" 1 contact name="dana" 0.98 2 timer duration="10 minute" 1.00 · after 1 step 2 refers to step 1, but takes nothing from it Run the plan as it stands? [y]es [n]o > y found dana@example.com 10 minutes, rings at 09:08 PM
A passed value fills a required input directly. For an optional one, evoke writes the value into
the step's words and decides the step again, under the same bar as any words. A reference that two fields could
satisfy stops with a line naming them. Recorded the same day, with the collection's timer.
What a step takes, in the manual →
Two reflexes from the collection
04Two reflexes you can install pass a path the same way.
download saves a file and declares the path it saved to. note takes a
quoted phrase, and «it» points back. Both run wherever Node runs. Recorded on 26 September 2026
against Jev, TypeSafe AI's model, on Linux.
$ evoke "download https://evoke.build/llms.txt and note it" 1 download url="https://evoke.build/llms.txt" 1.00 2 note · takes path from 1 saved llms.txt to ~/Downloads (12 kB) 2 note text="~/Downloads/llms.txt" 0.99 noted "~/Downloads/llms.txt" in ~/notes.txt
- 1«it» points at step 1.
noteneeds a quoted phrase your words did not give.download's result holds the path it saved to, of that kind, so the plan printstakes path from 1. Once the file is saved, the path fillstext. - 2Two lines in two manifests make the pass.
download's declares[yields]path = "quoted", andnote'stexthaspick = "quoted". Neither names the other. - 3The note is the path, dated.
~/notes.txtgained one line,2026-09-26 ~/Downloads/llms.txt, and the file sits in~/Downloads.
The plan for this sentence is also pinned in evoke's tests, over the whole collection, replayed from written answers. To run it yourself, install the two reflexes, name a notes file that exists, and type the sentence.
-
Install both from the collection
evoke add evoke-build/reflexes/downloadevoke add evoke-build/reflexes/note -
Name the notes file
noteappends to a file that exists, so make it first.touch ~/notes.txt && evoke config note file "~/notes.txt" -
Type the sentence
The plan prints first. Once the file is saved, step 2 prints again with the path, and the note is written.
evoke "download https://evoke.build/llms.txt and note it"
Set it up
05Set it up on a Mac, with a lookup you write.
No reflex in the collection looks up an address, so you write the lookup, and the collection's
mail takes its address. The draft opens in your mail app, and you send it yourself.
- A MacThe collection's
mailopens a draft in your mail app, and runs only on macOS. - evoke and a keyInstall evoke and set your key, then come back here.
- Node 24 or newerBoth reflexes run a JavaScript file.
- gitTo install
mailfrom the collection.
-
Make a project for your own reflexes
A folder with an
evoke.tomlfile is a project. When you type inside it, evoke uses only the reflexes installed there. The file's one line,adapter = "jev", has Jev, TypeSafe AI's model, read this project's sentences.mkdir -p ~/dev && cd ~/dev && echo 'adapter = "jev"' > evoke.tomlA project's files decide what runs, so evoke decides in a new one only after you trust it once. It answers
+ trusted ~/dev.evoke trust -
Make the lookup reflex
evoke newwrites a working reflex from a template: a manifest, a program and its types. It prints one line for each file it wrote incontact/.evoke new contact -
Write its manifest
Replace
contact/reflex.tomlwith this manifest. Its input,name, takes a word from your list of people.effect = "read"declares that it only looks something up. The two lines under[yields]declare that its result holds an email address, and they are what letsmailtake it.contact/reflex.tomlyoursreflex = 1 description = """ Look up a person's email address in your list of people. One person at a time; prints the address.""" not_for = ["writing to anyone", "phone numbers", "adding a person to the list"] tags = ["people"] effect = "read" confirm = "Look up {name}'s address?" run = "contact.mts" [args.name] ask = "Whose address?" vocab = "people" [yields] email = "email" [examples] "look up dana's address" = {} "what is sam's email" = {} "find the address for dana" = {} [tests] "dana's email please" = {} "email dana" = false
-
Write its program
Replace
contact/contact.mts. Your list of people is the directory here: in step 6 you store each person's address with their word, and evoke passes that address to the program. It returns the address twice: as the line you read, and as data for the next step.contact/contact.mtsyoursimport type { Reflex } from "./reflex.d.ts" export default (async ({ name }) => { // name is the word's value: the address you stored for that person. if (!name.includes("@")) throw new Error(`no address for ${name}`) return { text: `found ${name}`, data: { email: name } } }) satisfies Reflex
To use a company directory instead, look the word up there and return the same shape. Then check the reflex:
evoke checkreads the manifest, loads the program, and rewritesreflex.d.tsfor the new input.cd ~/dev/contact && evoke check -
Install both reflexes
Back in
~/dev, add your lookup by its folder, andmailfrom the collection by its path. Each prints one row for the reflex it installed.contactshows as inactive until your list of people has a word.cd ~/dev && evoke add ./contactevoke add evoke-build/reflexes/mail -
Add the people you write to
Add each person with a word, a meaning and their address, in place of the ones below. The meaning is what the classifier reads. The address goes only to your program, and the classifier never sees it.
evoke vocab people add dana "Dana Ortiz, operations." --value dana@example.comTo check the program without asking the classifier, call it by name. It prints the call, then
found dana@example.com.evoke run contact name=dana -
Look at the plan
evoke tryruns nothing. It prints the plan first: step 1,contactfor dana, then step 2,mail, which takes email from 1. Under each step come the classifier's answers for that part.evoke try "look up dana's address and email them" -
Type the sentence
Type it inside
~/dev. The plan prints first, thencontactprints the address. The address fillsmail'sto, and step 2 prints again with it. Your mail app opens a draft to Dana, and evoke printsnew mail towith the address. Nothing is sent until you press send. If evoke is less sure of the email step, it shows the call and asks first.evoke "look up dana's address and email them"A sentence with several steps makes one request to find its steps, then at least one for each step.
In your app
06The same sentence, from a Node application.
The SDK loads ~/dev, runs the sentence with weave(), and lets your code
answer its questions. It needs Node 24.5 or newer, and ES modules.
npm install @evoke-build/evoke// node draft.mts "look up dana's address and email them" import { createInterface } from "node:readline/promises" import { load } from "@evoke-build/evoke" const project = await load({ root: import.meta.dirname }) // ~/dev, with contact and mail const rl = createInterface({ input: process.stdin, output: process.stdout }) const yes = async (question: string) => (await rl.question(`${question} [y/n] `)) === "y" const woven = await project.weave(process.argv[2] ?? "", { confirm: d => yes(d.prompt.template), proceed: () => yes("Run the plan as it stands?"), }) rl.close() console.log(woven.status) // "ran", or the worst step's status console.log(woven.steps[1]?.bound) // [{ arg: "to", from: 1, field: "email", value: "…" }]
- 1
confirmruns when a step needs your yes, for instance when evoke is less sure than its bar.proceedruns when a step points at another it takes nothing from. - 2
woven.statusis the worst step's. Each step'sboundsays what it took: hereto, from step 1'semail.