Serenities AI™
Guides
GuideAccount Functions & Webhooks

Account Functions & Webhooks

Site functions belong to one site. Account functions belong to you — they can reach across the sites you give them (read one site’s orders, write another’s CRM) and are managed in the Automations section of your dashboard.

Inside an account function, ctx.sites gives you each of your sites’ tables, files, users, and exposed methods — so two sites that never knew about each other can work together.

Six ways they run

Open a function and you get tabs: Code, Runs (every run, what started it, its log), Triggers, Access and Settings. Everything below is set on the function’s own Triggers tab — Automations → All triggers only lists them across the account.

  • On a schedule — say it in plain English (“every weekday at 8am”) or pick a preset or a cron expression; as many schedules as you like.
  • Webhook — each function can get secret URLs; anything that POSTs to one runs the function with the request body as the event (POST only). The URL is a password: anyone holding it starts the function, so keep it private. Perfect for payment providers, form services, CI, or any external tool that “calls you back”.
  • Event (when something happens) — a row added, changed or deleted in a base (narrowed to which base, which table, only when a field changes, only rows where…), a file in Drive, a flow or agent run, a site event, a payment, a connector, or your own event raised from code or the chat. The function receives { event, config }.
  • Incoming mail (email address) — a unique email address that runs the function with the parsed email as input. See Email Triggers (Mailhooks).
  • Called by an automation or another function — automations can target account functions; functions can chain (ctx.functions.run). A function another function calls must say so under Access → Who can run this function, and it needs its own access rule for site users.
  • Started by an agent or an event from your site — an AI agent or event a site page starts runs as your workspace, inside that agent’s own sites, bases, tools and approvals. It never gets the visitor’s rights, and never more than you gave it.
  • Called from your published app — see below.

Who it runs as

A function, like a flow, runs as your workspace. Whoever builds it decides what it can do, and can only give it what they may use themselves. Whoever starts it is passed in as information, ctx.run.startedBy (the name of the person who started it, and in a workspace with members their role, or the agent and the person behind it), so the code can greet them or note who ran it. It never gives them anything more. A site visitor’s function still sees only what that visitor may see.

Because anyone who may run it gets everything it does, check what the person running it hands it — which row, which address, which amount — before acting on it. A flow does the same with an if step, and can be limited under Who can run this flow on its page.

Calling from your published app

Your app’s pages can invoke an account function with functions.invokeAccount('name', params). Because account functions are powerful (they can span sites), this is off by default — you must switch it on per function, in three steps:

  1. Grant the site: the function’s grants must include the site that calls it.
  2. Set who may call it: give the function execute access rules — for example “signed-in members”. No rules = no app access. “Anyone” works only through the sites the function is granted, never through any other site.
  3. Let the page call it: on the function’s Access tab, under Who can run this function, tick Visitors on the site page (directly). A new account function has it off, so a helper you made for other functions can’t be called from a page by accident.

What the function can see when a page calls it

  • Only what that person may see. Every row it reads or changes is judged like the person’s own request: a member of that table’s site under the table’s access rules, anyone else as a visitor. No rule on the table means refused.
  • Functions it calls keep the same limit. A calls B calls C never shows more than the person could see, and every function in the chain needs its own access rule for site users. A function with no rule is refused.
  • Full data access lifts the limit. On the function’s Access tab, under Powers & secrets. You can turn it on there yourself (in a workspace with members, the Owner or an Admin does). Your AI can also ask for it: it shows a card in the chat (or sends you a link) that says in plain words what would change, and nothing happens until you tap Apply and confirm it’s you. The AI can’t tap for you, and it can’t say it is on before you do. The same card is how the AI fixes such a function’s code or asks for a new secret, website or connected app. On a card about its code you can choose Apply, and let the AI keep fixing this function’s code; then code fixes go through at once with an Undo, and anything bigger still asks. You can see and switch that off on the function’s Access tab.
  • Your own runs are not limited. Runs you start, schedules, and webhooks that don’t carry a site sign-in see everything.
  • Visitors see one plain refusal (“You don’t have access to do that.”). You see the reason on the function’s Runs tab, for example that a function it calls has no access rule.

The signed-in member’s identity is checked against your rules automatically, and the function receives it as params._appUser. Serenities sets it from the member’s sign-in, never from the request: anything a caller sends under a name starting with _ is removed, so a request can’t pretend to be a member. A webhook URL works the same way and accepts POST only. Account functions read global environment variables — your account-level secrets (ctx.env.get('NAME') or {{NAME}} in ctx.fetch). These are deliberately separate from each site's env vars: site functions see only site values, account functions see only global values — no silent fallback between them. A function can only use a secret you have ticked for it: open the function, go to Access, then Powers & secrets, and tick it under “Secrets this function can read”. A new function has no powers on.

An account function is not a site function

It has no ctx.tables and no logged-in user, because it belongs to your account rather than to one site. Reach a site's data through ctx.sites['Site Name'].tables (granted sites only). A site keeps its tables in one base; this is how a page uses the data of a different base. If you're unsure what a function can see, have it return Object.keys(ctx) once — the AI can do this for you.

Calling a slow API (an AI model, a big import)? Two limits must both allow it: the request's own timeout and the function's timeout, which defaults to 10 seconds. Raise the function timeout for anything AI-related, or the call is cut off mid-flight.

Listing mailboxes: whose do you get?

ctx.email.accounts() answers “what may the person calling right now send through”. If a member is signed in you get only the mailboxes they connected — that isolation is the point, so one customer can never send as another. An empty list therefore means this member has none, not that you have none; the reply's scope tells you which it was. When your own code wants your account's shared mailboxes anyway — an admin screen, an ops function — ask for them explicitly with ctx.service.email.accounts(). It never returns a member's private mailbox.

Site function or account function?

Most logic belongs to a single site — write that as a backend function (code) or a data function (declarative). Reach for an account function only when the work spans sites or an external service needs a URL to call.

SituationUse
Logic for one site, called by its pagesSite function (backend or data)
External service needs a URL to callAccount function + webhook
Work spanning two or more of your sitesAccount function (ctx.sites) — across the sites you gave it; when a page calls it, only what that visitor may see

Call AI from a function

ctx.ai.generate runs one AI call with the account owner’s models: the platform models (billed as AI credits, like chat) or the owner’s own provider keys from Your AI (billed nothing here). One call, no tools, 60-second limit, up to 4,096 output tokens, quick low-effort thinking by default.

const { text } = await ctx.ai.generate({ prompt: 'Summarise in one line: ' + params.body, maxOutputTokens: 200 });

const r = await ctx.ai.generate({
  system: 'Reply with one word: billing, bug, or other.',
  messages: [{ role: 'user', content: params.ticket }],
  model: 'byoai:gpt-5.4@key:…',   // one of your own keys (Your AI); leave out for the platform model
  effort: 'low',                   // 'low' | 'medium' | 'high'
});
// r = { text, model, usage: { inputTokens, outputTokens }, credits, ownKey }

When the owner’s monthly AI credits are used up, platform-model calls fail with a message that says so; catch it and fall back.

Use built-in connectors

ctx.connections.list() and ctx.connections.call(credential, tool, args) run a saved service credential’s tools (Slack, Google, GitHub, Stripe…) from code — only the ones on the function’s own list, none until you add one on its Access tab (“Authorised connections”). The secret stays server-side. See built-in connectors.

Quotas & requirements

Running functions requires a paid plan. Account functions execute in the same sandboxed engine as project backend functions and draw from the same monthly execution quota and concurrency limit — there is no separate account-function allowance, so heavy cross-site automation counts against the same budget as your sites’ own functions.