Skip to main content
chat() answers a person and run() answers your code. .on() answers neither: it declares work that should happen without a caller at all.
lib/agent.ts
Nothing has run yet, and nothing has been written. .on() is a declaration — collected when the module loads, reconciled once when your process boots.

Every shape

{ event } is your own product event — the one you vendo.emit(...). { webhook } is a signed delivery from a connected service, which lands on your deployment’s own webhook door. The task is always a goal here: words, thought through by this agent. Step pipelines come from the chat and manifest doors, which know the tools.

Options

  • id — stable identity. Leave it out and the identity is a hash of the schedule and the task, which means editing either one mints a new automation and the old one is disarmed at the next reconcile. Set an id when you want to edit the words in place and keep the run history.
  • timezone — the zone cron and every are evaluated in. Unset is UTC.
  • budget — a ceiling on one firing, { maxToolCalls }. Unset is 50.

It fails at the declaration, not at 2am

Validation is synchronous, at the line you wrote it on, before your process serves a single request.
A cron nobody can run, an every outside <n><s|m|h|d>, an at that is not an instant, an unnamed event or webhook — each one stops the boot, with the nearest valid form to paste. An automation you cannot see failing is worse than a deploy that will not start.

Arm them

A declaration is inert until a lifecycle reconciles it. There are two, and they run the same reconcile. In a standalone backend, serve() is the lifecycle. It takes the agents whose declarations this process runs and hands back the handle that stops the scheduler:
close() stops the scheduler and nothing else: the records stay in the store, armed, because stopping a process is not a decision about what should fire. Inside a Vendo deployment, createVendo’s own boot is the lifecycle, and registering the agent is what arms it. An automation record stores the agent’s name, never the agent, and your code is looked up under that name when it fires:
app/api/vendo/[...vendo]/route.ts
vendo.agent — the embed’s own composed agent — is registered for you, and .on() works on it too. Two agents claiming one name throw at startup. A record naming an agent nobody registered writes a failed run that names the missing name; there is no fallback brain, because running the wrong agent under this record’s grants is worse than not running.

What a deploy does

Every boot reconciles what your source declares against what is stored for code-authored automations only. Chat-authored ones are untouched — a user’s automation is not yours to reconcile.
1

New declaration

Created, and armed.
2

Edited declaration

A new identity is created and armed; the one it replaced is disarmed. It is disarmed rather than deleted, so its run history survives.
3

Declaration deleted from your source

Disarmed. Consent was the code, and the code no longer says it.
4

Anything a person switched off

Left alone, forever. automations.disable(id, ctx) outranks your source, and no redeploy will re-arm it.

Authority

A goal runs with the owner’s grants, and nobody is present to be asked. So the permissions are settled once, when the automation is turned on:
Until they are, the firing stops loudly at the first call it does not hold — the run row names the permission it needed, and runs.rerun(runId) is the second half of that.

Where to go next

Automations

The whole model: records, the two authors, and what wakes them.Automations →

Run — run()

One unattended run, right now, with a typed answer.run() →