evoke

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

01

A 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.

~/home
$ evoke "look up dana's address and email them" 1 contact name="dana" 0.98 2 mail · takes email from 1 found dana@example.com 2 mail to="dana@example.com" 0.99 new mail to dana@example.com
  1. 1«them» points at step 1. mail needs an address your words did not give. contact's result holds one, of the kind mail takes, so the plan prints takes email from 1. Once contact runs, the address fills to, and your mail app opens the draft.
  2. 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. 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

02

The 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.

WhereThe lineWhat 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.mtsreturn { 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 plan2 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.
contact/reflex.tomlyours
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
contact/contact.mtsyours
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
vocab/people.tomlyour list of people
dana = { what = "Dana Ortiz, operations.", value = "dana@example.com" }
sam = { what = "Sam Lee, sales.", value = "sam@example.com" }
  1. 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.
  2. 2The lookup reads your list of people. dana and sam are 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.
  3. 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

03

A 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
$ 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

04

Two 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
$ 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. 1«it» points at step 1. note needs a quoted phrase your words did not give. download's result holds the path it saved to, of that kind, so the plan prints takes path from 1. Once the file is saved, the path fills text.
  2. 2Two lines in two manifests make the pass. download's declares [yields] path = "quoted", and note's text has pick = "quoted". Neither names the other.
  3. 3The note is the path, dated. ~/notes.txt gained 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.

  1. Install both from the collection

    evoke add evoke-build/reflexes/download
    evoke add evoke-build/reflexes/note
  2. Name the notes file

    note appends to a file that exists, so make it first.

    touch ~/notes.txt && evoke config note file "~/notes.txt"
  3. 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

05

Set 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.

  1. Make a project for your own reflexes

    A folder with an evoke.toml file 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.toml

    A 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
  2. Make the lookup reflex

    evoke new writes a working reflex from a template: a manifest, a program and its types. It prints one line for each file it wrote in contact/.

    evoke new contact
  3. Write its manifest

    Replace contact/reflex.toml with 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 lets mail take it.

    contact/reflex.tomlyours
    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
  4. 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.mtsyours
    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

    To use a company directory instead, look the word up there and return the same shape. Then check the reflex: evoke check reads the manifest, loads the program, and rewrites reflex.d.ts for the new input.

    cd ~/dev/contact && evoke check
  5. Install both reflexes

    Back in ~/dev, add your lookup by its folder, and mail from the collection by its path. Each prints one row for the reflex it installed. contact shows as inactive until your list of people has a word.

    cd ~/dev && evoke add ./contact
    evoke add evoke-build/reflexes/mail
  6. 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.com

    To 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
  7. Look at the plan

    evoke try runs nothing. It prints the plan first: step 1, contact for 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"
  8. Type the sentence

    Type it inside ~/dev. The plan prints first, then contact prints the address. The address fills mail's to, and step 2 prints again with it. Your mail app opens a draft to Dana, and evoke prints new mail to with 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

06

The 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
~/dev/draft.mtsyours
// 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. 1confirm runs when a step needs your yes, for instance when evoke is less sure than its bar. proceed runs when a step points at another it takes nothing from.
  2. 2woven.status is the worst step's. Each step's bound says what it took: here to, from step 1's email.

steps() and weave(), in the manual →