Examples · 06 of 06
One person asks. Another approves.
A payment is decided, then waits as plain data until a second person confirms it. A payee is one of your words, never invented, and paying is destructive, so it can never run on its own. A pattern for your app: who may approve is your check.
The request and the result
01The maker types. The checker says yes.
A decision travels from one person to another through a queue. Interpretation and execution are two moments, in two processes. Every line is what Node printed.
- 1The maker's sentence becomes a call. The payee and the account are words of the desk. The amount is a number read from the sentence, within its range.
- 2The decision waits as plain data. It carries the call, the effect, the weakest judgment and why it stopped. Any queue can hold it.
- 3Only
confirmed: trueruns it. The checker reads the call and the reason, then runs the same decision in another process.
The operations
02One reflex, two processes.
The payment reflex as a file and as code, the desk's payees, and the checker that runs a queued
decision. Each desk compiles its own words with with, in milliseconds.
reflex = 1 description = """ Pay an approved payee from one of our accounts. One transfer, released after a second person confirms it.""" not_for = [ "adding or changing a payee", "moving money between our own accounts", "asking a balance", ] tags = ["treasury"] effect = "destructive" confirm = "Pay {payee} {amount} from {account}?" run = "pay.mts" [config] api = "The payments API address" token = { about = "The payments API token", secret = true } [args.payee] ask = "Which payee?" vocab = "payees" [args.account] ask = "From which account?" vocab = "accounts" [args.amount] ask = "How much?" pick = "number" range = [1, 250000] [args.ref] ask = "What reference?" pick = "quoted" optional = true [examples] 'pay Acme 12400 from ops, ref "8812"' = { amount = "12400", ref = "8812" } "send 950 to the cleaners from the office account" = { amount = "950" } "wire 3200 to Acme from treasury" = { amount = "3200" } [tests] 'settle 18000 with Acme, ref "Q3"' = { amount = "18000", ref = "Q3" } "pay Acme from ops" = { amount = false } "add Acme as a new payee" = false
acme = { what = "Acme, the packaging supplier.", value = "py_acme" } cleaners = { what = "Blitz, the office cleaners.", value = "py_blitz" }
import { load, reflex } from "@evoke-build/evoke" import { jev } from "@evoke-build/evoke/jev" const pay = reflex({ description: "Pay an approved payee from one of our accounts.\n" + "One transfer, released after a second person confirms it.", not_for: [ "adding or changing a payee", "moving money between our own accounts", "asking a balance", ], effect: "destructive", confirm: "Pay {payee} {amount} from {account}?", args: { payee: { ask: "Which payee?", vocab: "payees" }, account: { ask: "From which account?", vocab: "accounts" }, amount: { ask: "How much?", pick: "number", range: [1, 250000] }, ref: { ask: "What reference?", pick: "quoted", optional: true }, }, examples: { 'pay Acme 12400 from ops, ref "8812"': { amount: "12400", ref: "8812" }, "send 950 to the cleaners from the office account": { amount: "950" }, }, }, async ({ payee, account, amount, ref }, { signal }) => { const response = await fetch(`${process.env.PAYMENTS_API}/transfers`, { method: "POST", headers: { authorization: `Bearer ${process.env.PAYMENTS_TOKEN}`, "content-type": "application/json", }, body: JSON.stringify({ from: account, to: payee, amount, reference: ref }), signal, }) if (!response.ok) throw new Error(`payments answered ${response.status}`) const { id } = (await response.json()) as { id: string } return { text: `paid ${amount} from ${account}, transfer ${id}`, data: { id } } }) // This desk's payees, from the database; nothing else is a word. const desk = (await load({ reflexes: { pay }, adapter: jev() })).with({ vocab: { payees: { acme: { what: "Acme, the packaging supplier.", value: "py_acme" }, cleaners: { what: "Blitz, the office cleaners.", value: "py_blitz" }, }, accounts: { ops: { what: "The operations account.", value: "acc_ops" }, office: { what: "The office account.", value: "acc_office" }, }, }, }) // then the maker and the checker, line for line as in checker.ts
// The desk decides; the decision waits in a queue as plain data; // a second person reads the call and runs it. import { load } from "@evoke-build/evoke" const desk = await load({ root: import.meta.dirname }) // maker const decided = await desk.decide('pay Acme 12400 from ops, ref "invoice 8812"') if (decided.outcome !== "confirm") throw new Error(decided.outcome) // through the queue and back: a decision is plain data const queued: typeof decided = JSON.parse(JSON.stringify(decided)) // checker: the call, then the effect, the weakest judgment and why it stopped console.log(queued.call) console.log(queued.prompt.own) console.log((await desk.run(queued, { confirmed: true })).text)
- 1A payee is a word. Its value is an id in the payments system. The classifier never sees the id, and no sentence can name a payee the desk did not list.
- 2The token is a secret. It is named in the settings and read from the environment for one run. It is never stored.
- 3The decision survives a round trip through JSON.
run()accepts it back, and refuses one made under another set of reflexes.
One boundary
03Missing the amount, it asks. Destructive, it asks again.
At a terminal the same reflex asks for what the sentence lacked, then confirms. A no is exit 2, and nothing was paid.
$ evoke "pay Acme from ops" How much? > 12400 pay payee="acme" account="ops" amount="12400" · destructive · weakest: route 1.00 Pay acme 12400 from ops? [y]es [n]o [t]each > n [2]
Sure at 1.00, and it still asked. There is no flag that skips a destructive reflex's question, at a terminal or in an app. What the classifier cannot do →
Before you run it
04What this pattern needs.
- Node 24.5 or newerAnd the SDK,
@evoke-build/evoke. - A payments APIIts address as a setting, its token as a secret from the environment. The body shown posts one transfer.
- Your wordsPayees and accounts, with the ids your system uses as values. From a file, or from your database.
- Authorization and approvalevoke holds the decision. Who may make one, who may confirm it, and where it waits are your application's.
The files above are the whole example. Copy them from this page.