Skip to main content
An automation is a record. It has an owner, a trigger, a task, and — if the task is a goal — the name of the agent that thinks it through. It lives in your database, it runs in your backend, and something outside just has to wake you up on time.

Two authors, one model

Someone has to say an automation should exist. Exactly two 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 revoking one 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 agent creates the record and tells them what it armed, in the thread. There is no form, no separate automations screen, and no create call for you to make — everything that can author one already does.
An automation card in a Maple thread reading Every Friday at 5:00 PM, prepare a digest of that week's spending by category, drafted and ready for you to send

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 reconciles once at boot. A bad schedule throws right there — "every monday" is not a cron, and you find that out at the declaration site rather than at 2am. See .on() for every shape it takes.

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" }
Host events fire in your own process, on the line that emitted them:
That runs every armed automation listening for 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, and the door is idempotent: a duplicate knock claims nothing and fires nothing.
With a Cloud key there is nothing to set. The deployment derives the tick secret from VENDO_API_KEY and publishes it, with its own URL, when it boots. Without a key, set VENDO_TICK_SECRET yourself and knock from your own cron.
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
A goal runs with the owner’s grants, inside your backend. A steps task needs no brain at all and runs in-process. A record naming an agent nobody registered writes a failed run with the missing name on it — never a silent skip, and never someone else’s agent under this record’s grants.

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, and after that the automation runs as the person who armed it, every time.

Reaching your API with nobody there

An away run carries no browser session. There is no cookie to forward, so auth material is minted for the owner before each outbound call — that is the actAs seam. Present calls need none of it: a user is in the room, and the inbound cookie or bearer already forwards. createVendo({ auth: authJs() }) fills actAs for you — see Wire auth — and most deployments stop reading here. The presets below are the same wiring by hand, for when no shipped auth preset fits. Auth.js and Supabase let a host holding the session secret mint a token their own verifier accepts, so nothing else changes in your app. Clerk and Auth0 sign sessions with private keys you do not hold, so their presets ship two halves: a producer that signs a short-lived VendoAway token, and a verify middleware you mount on your host app. They ship on @vendoai/vendo/actions, a subpath of the package you already have:
lib/vendo.ts
The cookie name doubles as the JWE salt, so it has to match your Auth.js config. Clerk and Auth0 return a preset object instead of a function; pass its actAs half.

Mount the verify middleware

Clerk and Auth0 sign a host-owned token rather than a real provider session, so your host API has to accept it and turn it back into a user.
middleware.ts
Each preset also exports expressMiddleware. The middleware strips any caller-supplied x-vendo-away-* headers, verifies a real VendoAway token, and injects the subject on headers your API can trust. Generate the shared secret with openssl rand -base64 32 and set it for both halves.

The impersonation guard

Before invoking actAs, Vendo compares the grant’s subject to the running principal’s. On a mismatch the call fails closed with act-as-subject-mismatch and no outbound request is made. You do not wire this; it applies on every preset, and every attempt is audited with its disposition: minted, declined, mismatch, or error.

Observe and control

In the browser there is nothing to build. One hook carries every verb into a panel you write yourself.
Server-side there is no request to read from, so every verb takes the caller’s RunContext last. That is what scopes the read to what this principal may see — there is no ambient “current user”.
disable is a person’s decision, and it outranks your code: a redeploy’s reconcile will never re-arm something a human switched off.