Two authors, one model
Someone has to say an automation should exist. There are exactly two who can, and they differ only in what consent means.A user, in chat
They ask for it in words. Consent is the grants they allow while they
are present — and they can revoke any of them later, which stops the run
loudly rather than silently widening it.
A developer, in code
agent.on(...) in your source. Consent is the code: it exists because
you deployed it, and your next deploy reconciles it.A user asks for it

The receipt is a card in the thread, not a config screen.
You declare it in code
lib/agent.ts
.on() is a declaration. It returns nothing, touches no database, and is
collected at module load; the reconcile happens once at boot. A bad schedule
throws right there, before your process serves anything — "every monday" is
not a cron, and you find that out at the declaration site rather than at 2am.
See .on() for the full surface.
What wakes it
Your deployment decides what is due. Nothing else does.Schedule
A five-field cron, a plain interval, or a one-shot timestamp.
"0 8 * * 1-5" · { every: "15m" } · { at: "2026-09-01T09:00Z" }Host event
Your own product event, emitted from the code path that owns it.
{ event: "invoice.paid" }Webhook
A signed delivery from a connected service.
{ webhook: "stripe" }invoice.paid — the emitting
user’s, and those of every org they belong to — and answers with the run ids it
started.
Schedules need someone to knock. One door does it, and it is idempotent:
VENDO_API_KEY and publishes it, with its own URL, when it boots. Without one,
set VENDO_TICK_SECRET yourself. The door takes a bearer token or a
standard-webhooks signature, and both are checked against that one secret — with
neither a key nor a secret, every knock is refused.
Cloud’s heartbeat is an alarm clock, not a brain. It calls
/api/vendo/tick
on every enrolled deployment once a minute with a signed, empty body. It holds
no schedule, decides nothing about what is due, and never writes a run. The
run ledger you read in the console is the one your deployment wrote.Agents are code, never stored
A record names an agent with a string. The agent itself is your code, registered under that name when your process boots and looked up when the automation fires.lib/vendo.ts
Permissions before the first fire
Nobody is there to approve anything at 2am, so the asking happens when the automation is turned on.missing is what the owner still has to allow; they belong to one grant set, so
one decision settles them all. After that the automation runs as the person who
armed it, every time — until they revoke something, and then the next run fails
loudly with the permission it needed named on it, and one tap runs it again.
Observe and control
In the browser there is nothing to build: the wire resolves the context from the request, and one hook carries every verb into a panel you write yourself.RunContext last — that is what scopes the read to what this principal may see,
so there is no ambient “current user”:
app filter, because a record holds no app reference. An app page
filters by resolving its own automations list and dropping the dead ids.
disable is a person’s decision, and it outranks your code: a redeploy’s
reconcile will never re-arm something a human switched off.
Where to go next
Declare one in code
agent.on(...) — every shape, and what a redeploy does..on() →API tools
Your own routes, extracted into a guarded tool set.API tools →
Guard
Risk grade, approval, and an audit line on every call.How Vendo works →