evoke

Give your scripts and your app a way to be asked.

Turn the scripts and commands you already run, and the functions in your app, into natural-language commands. Each one becomes a reflex. A reflex is a small program you install and ask for in your words. Here, the program is your own script, command or function. Its manifest, a short file named reflex.toml, describes in plain words what it does and what it needs. For a command you already run, that file is all you write.

In a Node app, the TypeScript SDK turns a person's sentence into a decision: a typed value that names the reflex and its values. Your code inspects it before anything runs.

Wrapping a script or a command? Read 01, 02, 06 and 07. Adding natural-language commands to a Node app? Read 03 to 05 as well. 08 shows how evoke relates to scripts, workflow tools and LLM tool calling.

Your command or function

01

Start with a command or a function you already have.

A command you already run needs only a manifest, and no code. A function needs the same manifest and a file whose default export is your function.

scale/reflex.tomla manifest, no code
reflex = 1

description = """
Scale a service to a number of replicas.
One deployment in one environment; the number is set, never added to."""
not_for = [
  "restarting or rolling back a service",
  "resizing a database or a disk",
  "autoscaling rules",
]
tags    = ["ops"]
effect  = "write"
confirm = "Scale {service} in {env} to {replicas}?"
run     = [
  "kubectl", "-n", "{env}", "scale", "deployment", "{service}",
  "--replicas", "{replicas}",
]

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

[args.env]
ask   = "Which environment?"
vocab = "envs"

[args.replicas]
ask   = "How many replicas?"
pick  = "number"
range = [1, 20]

[examples]
"scale checkout to 6 in staging"   = { replicas = "6" }
"run 3 replicas of search in prod" = { replicas = "3" }
"take payments down to 1 replica"  = { replicas = "1" }

[tests]
"bring checkout up to 12 in prod" = { replicas = "12" }
"scale checkout"                  = { replicas = false }
"restart checkout"                = false
  1. 1The program runs directly. run lists a program and its arguments. evoke starts kubectl with those arguments directly, never through a shell. No Node or other runtime is involved.
  2. 2Each placeholder is one whole argument. {service} becomes a word from your own list of service names, or that word's value when you gave it one. You fill the list once, with evoke vocab services add. {replicas} becomes the number read from the sentence. A value that would start with - is refused, so nothing typed can become an option of the command.
  3. 3What it prints is the result. A non-zero exit is a failure. Settings, which a manifest lists under [config], reach the program as environment variables named EVOKE_CONFIG_<KEY>. The typed sentence arrives as EVOKE_INPUT.
  4. 4Your own script works the same way. Name it by its absolute path, or put it on your PATH. A yes/no switch, or an argument with literal braces, needs a program written as a .mts or .mjs file instead.

The first tab is the scale reflex of the kubectl example, where a team asks for it by its own service names. In the second, evoke new writes a reflex that already works, and the program is your function: before evoke run, add it with evoke add ./hello, from the directory that holds hello. How evoke runs a command, without a shell →

What stays your responsibility

  • The program. A command with its arguments, or a .mts or .mjs file whose default export is a function. A file must carry everything it needs. Bundle anything beyond Node's built-in modules and Web APIs into it before you commit it.
  • Its result. One lowercase line a person reads. A file may return { text, data } when an app needs more. A thrown error or a non-zero exit is a failure.
  • Its deadline. One decision has 30 seconds, from reading the sentence to the program's result. Time spent waiting for a person's answer does not count. A program in a file receives a signal that fires at the deadline. When it fires, the program must stop.
  • Its platform. A program that runs on one system checks that first, and fails with a plain message elsewhere.
  • What it may do. A reflex runs with the permissions of whoever runs it, held by the kernel to what its manifest declares under [needs]: the paths it reads and writes, whether it reaches the network, the programs it runs. The effect you declare states whether the program only reads, makes a change that can be undone, or makes one that cannot. It is a promise to everyone who installs it.

evoke run calls a reflex you added, by name. Reading a typed sentence takes a network request and an API key. evoke run reads none, so you can build and try a reflex offline, without a key. evoke check names each line of the manifest to fix, and rewrites reflex.d.ts, which types a file's arguments. A reflex that runs a command needs no Node, only the evoke binary, on macOS or Linux.

Install evoke in your terminal → Write your first reflex →

The manifest

02

Describe what it does, and where it stops.

A classifier reads each sentence against your manifest. The classifier is Jev, TypeSafe AI's model. It answers closed questions, such as which reflex and which value, and gives each answer a probability.

lights/reflex.tomlthe whole shape, an illustration
reflex = 1

description = """
Turn the lights in one room on, off, or dim them.
Ceiling and lamp lights only."""
not_for = ["colour scenes and schedules", "asking whether a light is on"]
tags    = ["home", "lighting"]
effect  = "write"
confirm = "Set the {room} lights {state}?"
run     = "lights.mts"

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

[args.room]
ask   = "Which room?"
vocab = "rooms"

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

[args.brightness]
ask      = "How bright, in percent?"
pick     = "number"
range    = [1, 100]
optional = true

[yields]
level = "number"

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

[tests]
"make it darker in here" = { state = "dim" }
"light a candle"         = false
description · examples
Your wording decides how well sentences find your reflex. The classifier knows your reflex only through its manifest's words. Examples are sent with each request, and they teach it. The classifier's provider reports its best accuracy in English.
options · vocab · pick · flag · takes
The five sources of an input. Each value comes from one of five places: the author's own list, the user's own list of words, a piece of the sentence read word for word, a yes/no switch, or the whole result of an earlier step in the same sentence. A number or a duration is also checked against its range.
one source each
Valid is not the same as understood. Each input takes exactly one of the five sources. evoke guarantees that every value is one the reflex allows. It cannot guarantee that it read you right. That is why a confidence bar, set by what the program changes, decides what runs without a question.
ask · the meanings
Questions a person could answer. The classifier reads the ask line, and evoke asks it when the value is missing. Give each option one short meaning, distinct from the others.
not_for · tests
The near misses. Name what sounds like this reflex but is not. Tests are never sent as examples, and evoke test runs them. A false test must never choose this reflex.
effect
Your honest claim. A read only looks. A write can be undone. A destructive program cannot, so it always asks first. A manifest with no effect counts as destructive.
config · confirm · tags
Settings, a question, and filters. Each user sets the [config] values once, with evoke config. A secret, such as token, is only ever taken from an environment variable. confirm is the question evoke asks when it needs a yes. tags let a person narrow a decision with --tag.
[yields] · returns
What the result holds, for a later step. It names a field of the result and its kind, such as a number or an email address. A later step of the same sentence can then take it. A reflex names its whole result with returns, and a later reflex takes it by that name.

lights is an illustration from evoke's own tests, not a reflex you can install. It shows every top-level key a manifest may carry.

The reflex.toml format and its JSON Schema → The rules for writing one →

Decisions in your app

03

Put decisions inside your app.

A decision is a typed value your code holds before anything runs. Inspect it, show it, fill in what is missing, ask for approval, then run it. Or let handle() run the whole loop.

npm install @evoke-build/evoke

Node 24.5 or newer, and ES modules. evoke's engine ships inside the package as WebAssembly, so there is no build step and no other package to install.

app.ts
import { load } from "@evoke-build/evoke"
import type { Reflexes } from "./evoke.d.ts"

const project = await load<Reflexes>({ root: import.meta.dirname })
const d = await project.decide("kill the lights in the den")

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
}

// typed by evoke.d.ts once the reflex is known
if (d.outcome === "run" && d.reflex === "lights") {
  d.values.room    // string
  d.values.state   // "on" | "off" | "dim"
}

lights is the illustration from part 02.

A decision's confidence is the lowest probability among its answers. A reflex that only reads needs 0.8 or more to run without a question, and one that writes needs 0.9. A destructive reflex always asks, however sure. These are the defaults. You can change them in evoke.toml, a file you own, or in code.

run
Ready. The call, the typed values, the confidence, and every answer behind it.
confirm
Needs a yes. All of the above, the reflex's own question, and every reason it stopped.
ask
Needs a value. What is missing, why, and what it may be. fill() reads the answer, types it, and checks the bar again.
abstain
Nothing fits. The ranking, so your app can show it or queue the request.
any decision
Plain JSON. A decision can wait in a queue or a database. It can run later, over the same reflexes and the same words. A decision that needs a yes runs only with { confirmed: true }.

Two examples build on this. In the triage example, what is sure runs and the rest waits in a queue for a person. In the maker-checker approval example, one person types a payment and a second person confirms it.

Use the SDK → The decision types, key by key → Approval set by risk: read, write, destructive →

Several steps

04

Run several steps, planned by evoke or by your loop.

One sentence can ask for several reflexes, and evoke shows the plan before anything runs. Or a program names the next request, and your own loop sends it.

A weave: one step's result feeds the next

One sentence read as several steps is a weave. A reflex declares under [yields] what its result holds, such as an email address. A later step takes that value when its words point back to it, as them does, and it lacks a value of that kind. A reflex can also return its whole result under a name, for a later reflex that takes that name. steps() reads a sentence into its plan and runs nothing. weave() runs the plan under your handlers.

A continuation: your loop decides

A program returns { text, data: { next } }. Here next is a field of your own choosing that holds the following request. Your app sends it. Each request is a decision like any other, checked against its own bar.

steps and weave
const plan = await project.steps("look up dana's address and email them")
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.verdict     // { outcome: "run" | "ask" | "confirm" | "refuse", because?: [...] }

const woven = await project.weave(input, {
  ask: (d, turn) => ui.pick(d.missing, turn.step),     // before anything runs
  confirm: (d, turn) => ui.confirm(d.prompt.template),   // at the step's turn
})
woven.status     // the worst step's: "ran", "failed", "declined", "refused", "unanswered"

contact in this plan is a stand-in from evoke's own tests. In the collection, download and screenshot yield the path they saved to, and note takes it. A sentence with several steps makes one request to find the steps, then at least one for each step.

Pass a result to the next step → The loop, in the runbook example. How one sentence becomes a plan →

Each team's words

05

Keep each team's names apart from the program.

Your data supplies the words people type, such as room or service names. The program underneath never changes. A server for many teams loads the reflexes once. It gives each team its own words with one synchronous call, with({ vocab }).

server.ts
const tenant = project.with({ vocab: { rooms: await db.rooms(user) } })
const d = await tenant.decide(input)
if (d.outcome === "run") await tenant.run(d)
vocab/rooms.tomlthe same words, as a file
den    = "The TV room downstairs; also 'the snug'."
office = { what = "The upstairs study.", value = "group-7" }
  • A word has a meaning, and may have a value. The classifier reads the meaning. The program receives the value, or the word itself when no value is set.
  • The value stays with you. evoke never sends a word's value to the classifier. It can be a path, a URL or a device id.
  • A decision is bound to its words. A decision made with one team's words is refused when it runs with another team's words. So a server cannot mix them up.
  • Files or data, the same shape. On a laptop, the words are a file. In an app, they can be files at the app's root or rows from your own database.

Give each team its own words, in the SDK manual → How an app keeps its reflexes and words →

Three kinds of test

06

Test how sentences are read, and what the program does.

Three tests, each for a different thing. One checks the wording over everything installed. One runs your app's control flow offline. One calls the program as a plain function.

the wording
$ evoke test
  lights  3 passed · 1 failed
    "make it darker in here"  state: expected "dim", read "off"
  timer   8 passed
  1 of 12 cases failed  →  evoke test
[1]

Every example and every test is decided over everything installed. So a test fails when another installed reflex would take your sentence. The classifier's provider trains its probabilities to be calibrated: across many answers, those given 0.85 should be right about 85 times in 100. evoke shows every one; evoke test checks your own sentences, and evoke calibrate measures that claim on your own records. The lights results come from evoke's own tests.

the control flow, offline
const record = process.env.RECORD ? jev() : undefined
const project = await load({
  reflexes: { timer },
  adapter: replay("answers.toml", { record }),
})
const d = await project.decide("timer for 10 minutes")
assert.equal(d.outcome, "run")

One run records the answers. Every run after that is offline and gives the same result. The recording carries the bars, so a test is gated exactly as production is. The CLI can replay the same recording, so a CI job can test your reflexes without an API key.

the program, as a function
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')

A program in a file is a function. Import it and call it with a context of your own. No part of evoke runs.

Write examples and tests → Testing in the SDK →

Share with a git tag

07

Share a reflex with a git tag.

Publishing is a git tag, with no account, no registry and no build step. Others install from your repository, pinned by tag, commit and content.

git tag v1.2.0 && git push --tags
install, from evoke's own tests
$ 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>

radhi/home/lights and radhi/timer are illustrations from evoke's own tests. You cannot install them. Put your own repository in their place.

evoke check · the next version
  lights  write  runs lights.mts
+ reflex.d.ts
  1.2.0 → 2.0.0  major
    args.state  renamed power
  1. 1Any new tag can improve the wording. A change to what the program takes or runs, such as an input or the run line, raises the version. evoke check compares the manifest with your last tag, and names the level the next tag must carry: same for wording only, minor for an addition, major for a change that users' files or calls may not survive.
  2. 2People keep their own wording. Whoever installs your reflex can reword it in an overlay, a file of their own laid over your manifest. When you rename an input, list its old name under was, and every overlay follows. An update reports what changed and never rewrites their files.
  3. 3Installing is a trust decision. No code runs at install, and a lock file pins exactly what was read, the declaration included. After that, a reflex runs with its user's permissions, held by the kernel to what it declared.

evoke's own collection, fifteen reflexes for macOS, installs the same way: evoke add evoke-build/reflexes.

Publish a reflex → What you trust when you install a reflex →

Beside what you use

08

How evoke relates to what you already use.

evoke turns a typed sentence into a call of a program you installed. It asks when something is missing or unclear, and runs nothing when nothing fits. Here is how that job sits beside what you already have.

You haveHow evoke relates
Scripts and shell aliasesA reflex wraps a script or a command you already have. You ask for it in your words instead of remembering its name and flags, and evoke asks for any value you left out. An existing command needs only a manifest, and no code. Nothing runs that nobody wrote a reflex for.
APIsA reflex's program can call any API you use. evoke decides which call a sentence means, and with which values. A key the API needs stays in your environment, and reaches the program for one run.
Workflow toolsThose run flows you build ahead of time, often when a trigger fires. evoke runs what one typed sentence asks for, now. It has no triggers, schedules or hosted runs.
LLM tool callingLLM tool calling, also called function calling, does the same step as evoke: a sentence becomes a call of your own code. With evoke, every value comes from a fixed source, each answer carries a probability, and reflexes are shared by git tag. A bar set by what the program changes decides what runs without a question, and your code can inspect each decision first. Use a model instead when a value must be composed, when you need a conversation, or when the task is open-ended.
Intents and slotsA reflex is like an intent with its slots and sample sentences, packaged with its program and installed from git. Its examples are the sample sentences, and its inputs are the slots. A vocabulary is a slot list that each user owns. The manifest's words go with every request, so a new example takes effect from the next sentence you type.

This site claims no advantage in speed or cost.

In the SDK, you can plug in any classifier that answers the same closed questions with probabilities, and keep your manifests as they are. The command-line tool uses only its own built-in connections to Jev.

How the classifier reads a sentence →

Start

Start with what you already have.

Wrap one command in your terminal today, or put decisions inside a Node app.

In your terminal

One binary for macOS or Linux, and a key for the classifier. A reflex that runs a command needs no Node.

In your app

Node 24.5 or newer, and ES modules.

npm install @evoke-build/evoke

evoke is free and open source. Each decision is one request to the classifier, billed by its provider to your own key. At the terminal, a sentence you repeat is answered from a local cache. Nothing goes to evoke.build.

What leaves your machine → Questions before you install →