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.
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=1description="""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
hello/hello.mtsyour code
importtype{ Reflex }from"./reflex.d.ts"exportdefault(async({ who })=>`Hello, ${who}!`)satisfies Reflex
a terminal
$evoke new hello+ hello/reflex.toml
+ hello/hello.mts
+ hello/reflex.d.ts$cd hello && evoke checkhello read runs hello.mts$evoke run hello who=Adahello who="Ada"Hello, Ada!
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.
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.
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.
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.
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=1description="""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.
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"importtype{ 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);breakcase"confirm":if(await ui.confirm(d.prompt.template))await project.run(d,{confirmed:true});breakcase"ask": project.fill(d,await ui.pick(d.missing));breakcase"abstain": ui.say(d.contenders);break}// typed by evoke.d.ts once the reflex is knownif(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.
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.
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.
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.
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 testlights 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():undefinedconst 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.
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
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.
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.
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.
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 have
How evoke relates
Scripts and shell aliases
A 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.
APIs
A 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 tools
Those 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 calling
LLM 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 slots
A 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.
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.