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:
| Import | Holds |
|---|---|
@evoke-build/evoke | load, reflex, the types, the errors |
@evoke-build/evoke/jev | jev(), the first adapter |
@evoke-build/evoke/openjev | openjev(), the same model through OpenJEV, an independent service |
@evoke-build/evoke/clef | clef() and clefFlash(), Cloudflare's Clef on Workers AI |
@evoke-build/evoke/testing | replay(), 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, withoutrunandconfig. 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 returnsran,abstained,declinedorunanswered.
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.