Twenty-eight systems ship in the engine. Each is optional, and skipping one costs only the jobs that needed it: a skill whose variables are unset reports itself unavailable and the run carries on.
Every system is the same three moves in a different order.
On the other system: an API token, an OAuth refresh token, or — for Google's own marketing APIs — an invitation rather than a key.
set-secret for anything secret, set-env for ids, hosts and addresses. Values never appear on screen or in your shell history.
A file in systems/. Without it the agent does not know the credential exists, and no job will use it.
The CLI walks through the common ones, printing what to do in order with the commands filled in:
npx @meffecta/agent connect # what can be connected npx @meffecta/agent connect gmail # what to do, in order, with the commands
There are walk-throughs for gmail, outlook, ga4, grafana, posthog, serper, hubspot, kleer, mongodb, cloudflare and inbox. Everything else on this page is a token you paste and a register entry you write.
Step 3 is the one people skip. The engine's skills are shared across every deployment and deliberately know no deployment's variable names — a skill says what kind of credential it needs and comes to your register for the name. So a perfectly valid token with no register entry is a system the agent will tell you it does not have.
One file per system in systems/. Strict frontmatter, prose below.
systems/cloudflare.md
---
system: Cloudflare — DNS and Pages for every domain we run
skill: cloudflare
requires: CLOUDFLARE_API_KEY, CLOUDFLARE_ACCOUNT_ID
access: read; DNS changes only when a job says so explicitly
probe: list zones
---
Zones live under one account. The Pages projects are named after the sites they
serve, so acme-www is acme.com. Cache purges are safe; DNS edits are not.
| system | What it is, in your words. This is what a person reads when deciding whether a job should run unattended. |
| skill | Which skill reaches it — a name from the catalogue. Or none, and say how it is reached instead. |
| requires | Exact variable names, all of which must be set. none for a system that authenticates as the runtime service account. |
| selectors | Optional. Per-project pointers, one per world — an unset one means that project is not wired up, not that the system is broken. |
| access | What a job may do, written honestly: "read-only", "may send mail", "may change DNS". |
| probe | The cheapest read that proves the credential works, with no side effect — list domains rather than send, read quota rather than generate. |
The prose below the frontmatter is what this deployment wants a run to know about that system, and it outranks the agent's assumptions. It is the right place for the things only you know: which portal, which database, what the naming means, what not to touch.
Fields mean the same thing in every deployment, so their definitions live in the engine's baseline prompt rather than in your repo. You write entries; you never have to document the format.
A skill's name says what it can do to the system. That is deliberate.
| query-<source> | reads only query-grafana, query-hubspot, query-gmail. The prefix is a class, not a disambiguator: it says this skill never writes, which is worth seeing at a glance in a list where most entries touch someone's production system. |
| <system> | Reads and writes: cloudflare, linear, webflow, google-drive. No prefix would be honest about them. |
| manage-<system> | writes manage-google-ads, manage-hubspot. The write half of a system whose read half is separate. |
| <verb> | An action: send-email, spawn-job. |
Where a system is both — records you query and a system you change — the two halves are two
skills, and the split is a safety boundary rather than a taste question. A reporting
job loads query-hubspot and never manage-hubspot, so the ability
to change the CRM is not in its context at all.
A skill's name is also its override key: a folder of the same name in your content repo — or in a working repo — replaces the built-in one. Matched by directory name, so renaming one silently stops it overriding anything.
All 28, with the exact variables each reads. Set none of them and nothing breaks.
Send or draft mail from the agent's own account — new messages, in-thread replies, drafts for review, attachments and inline images including rendered charts.
Read the mailbox the agent serves — search inbox and sent mail, read a body, list drafts, list a day's calendar events.
--account NAMECreate and update calendar events — in a Workspace calendar via domain-wide delegation, or a private one via OAuth.
Read and write Drive, Sheets, Docs and Slides — search, read, export as text, create documents, append rows.
Read a Microsoft 365 mailbox and calendar through Graph. Read-only by design — it cannot send.
Inboxes the agent owns, made over an API — so a job can have an address of its own and answer on it. Read a thread, reply in it, send new mail, attach files.
Search Console, Google Analytics 4 and Google Ads performance in one skill — which queries bring clicks, where traffic came from, what a campaign spent.
Production product analytics — did anyone do X, how many, when, funnels and adoption, and what a capture actually contains.
Google's live search results as JSON — organic rankings with positions, People Also Ask, news with a time filter, places. The built-in web search returns neither positions nor a time filter, so rank tracking and competitor research need this.
A domain's Ahrefs Domain Rating and the Ahrefs crawler IP list, via the free public API.
A full SEO, GEO and AEO audit of a URL or site — classic search, AI-answer engines, and answer-engine readiness. Named after the work, not a system.
Production logs from Grafana Cloud (Loki) — the server side of an incident, traced by id.
logs:readCoralogix logs with DataPrime or Lucene — filter by application, subsystem, severity and time, and aggregate.
Full-text and filtered search over indices, aggregations, and the mappings that say what a field actually is.
encoded value from Elastic's create-API-key response. Scope the key to the indices jobs read; that is what actually stops a job reading the restSQL against a BigQuery warehouse, plus the dataset, table and schema inspection to write it correctly.
List collections, inspect a collection's real shape, find and filter documents, and aggregate for counts, groupings and time series.
Contacts, companies, deals, tickets, notes and calls, plus owners, pipelines and the custom properties a portal has defined.
Create or update a contact, company or deal, move a deal to another stage, log a note or a call, associate records.
Kleer accounting and payroll (the Swedish Finance-as-a-Service platform) — client invoices and payment status, supplier invoices awaiting certification, the ledger.
Post a message or a threaded reply, add a reaction, read a channel's history or a thread, look up a user or channel.
Read and change a Linear workspace — issues, projects, cycles, roadmaps, comments. Search by team, state, assignee, label or date; read what shipped in a cycle; create an issue.
Bearer — unlike every other credential here, which is the usual reason a first call 401s. A second workspace follows LINEAR_<NAME>_KEYRead and change a Webflow site — pages, CMS collections and their items — and publish it.
cms:read to audit, cms:write to edit, sites:write to publish. A token without sites:write cannot put anything on the public internet, which is a better guarantee than a rule in a promptThe visual-management platform: boards hold screens, screens hold modules — KPI tiles, charts, tables, timers, safety crosses. Build a board over the API, then photograph the rendered board through a browser.
Zones, DNS records, cache purge, Pages projects and deployments, certificate checks.
A real headless Chromium (Playwright, preinstalled) — log into and read web services that have no API, keep sessions alive across runs, take screenshots, render a page to an image.
Pause a campaign, change a bidding goal, edit targeting, add negative keywords, lower a budget, create a Search campaign. The only skill that spends money by writing — every mutation is dry-run first, capped, and written to the audit bucket, in code rather than in a prompt.
Work on an ISO/IEC 27001 information security management system — review the document set against the standard, find what is overdue or inconsistent, draft revisions. Generic, because the standard is identical for every company; where your ISMS lives is a register entry.
Hand work to a later run of the same job — a follow-up with a fresh time limit, optionally at a chosen moment, whose instruction the agent writes itself.
One credential, several projects. The prefix is yours to choose.
Variables come in three classes, and the difference matters when you add a second product or client:
| Core plumbing | AGENT_*, GIT_REPO_URL, GITHUB_TOKEN, CLAUDE_CODE_OAUTH_TOKEN. The runner itself, validated at boot. |
| Shared logins | One credential covering many projects: GRAFANA_*, POSTHOG_*, GMAIL_*, CLOUDFLARE_*, GOOGLE_ADS_DEVELOPER_TOKEN. No prefix. |
| Selectors | Which project to point a shared credential at: ACME_GA4_PROPERTY_ID, ACME_POSTHOG_PROJECT_ID, ACME_GOOGLE_ADS_CUSTOMER_ID. The ACME_ is yours — name it after the world, and have that world's file say which selector belongs to it. |
npx @meffecta/agent set-env ACME_GA4_PROPERTY_ID=123456789 npx @meffecta/agent set-env ACME_POSTHOG_PROJECT_ID=12345 npx @meffecta/agent set-env ACME_GOOGLE_ADS_CUSTOMER_ID=1234567890
Selectors are not validated and never have to be declared to the engine — they pass
straight through to runs. A skill that reads a bare name (GA4_PROPERTY_ID)
is told by the register to bridge the world's variable into it first.
A new project does not get env structure by default, and should not: the job names
the world, the world's file names the selector, and the shared credential is already there.
Adding a second product is usually three set-env lines and a world file.
The longest set-up here, and the only one where a step can take days. Worth doing in order.
Ads is unusual twice over. There is no key to paste — the agent authenticates as its own Google Cloud identity — and there is a developer token, which is not a credential for your account but permission to use the API at all, and Google reviews it.
A manager account (still widely called an MCC) at ads.google.com → manager accounts. From it, invite the Ads account the agent should work on, and accept the invitation inside that account.
This is not optional bureaucracy: the developer token lives on the manager, and every
API call sends the manager's id as login-customer-id. Both ids are
digits only, no dashes.
In the manager account: Tools → Setup → API Center. The token is issued immediately, and by default it has test-account access — it works against Google's test accounts and returns an error on a real one, which is the single most common "my token is wrong" that is not a wrong token.
Apply from the same page for Basic access, which is what makes it work against your real account. Google reviews the application; it is normally days rather than hours. Standard access raises the daily operation limits and is worth asking for only once you know you need it.
Everything else here can be set up while you wait.
The agent authenticates as the service account its Cloud Run service runs as. Get its
address, which is the acts as line:
npx @meffecta/agent status
Then in the Ads account: Admin → Access and security → + → invite that address. Read only is right for reporting. Only raise it to Standard if you actually intend jobs to change the account — that single choice is what decides whether the agent can spend money, and it is enforced by Google rather than by a prompt.
This is a Cloud Run capability: the token comes from Google's metadata server, which exists there and nowhere else. On a local machine the skill reports itself unavailable and the job goes on.
npx @meffecta/agent set-secret GOOGLE_ADS_DEVELOPER_TOKEN npx @meffecta/agent set-env GOOGLE_ADS_MANAGER_CUSTOMER_ID=1234567890 npx @meffecta/agent set-env ACME_GOOGLE_ADS_CUSTOMER_ID=9876543210
Two ids, scoped differently, and getting this backwards is the classic failure. The
ad account is per world and takes your prefix. The manager id is shared
across every world and takes no prefix — there is no
ACME_GOOGLE_ADS_MANAGER_CUSTOMER_ID, and inventing one silently blanks the
real value.
systems/google-ads.md
---
system: Google Ads — the Acme search and PMax account
skill: query-google-marketing
requires: GOOGLE_ADS_DEVELOPER_TOKEN, GOOGLE_ADS_MANAGER_CUSTOMER_ID
selectors: ACME_GOOGLE_ADS_CUSTOMER_ID
access: read-only. Changes go through manage-google-ads and are proposed, never applied
probe: SELECT campaign.name FROM campaign LIMIT 1
---
Bridge the world's customer id into GOOGLE_ADS_CUSTOMER_ID before calling; the
manager id is read directly and never prefixed. Costs come back in micros.
Writing skill: query-google-marketing and saying so in access:
is how you keep a deployment read-only. Naming manage-google-ads is how you
opt in — and even then every mutation is dry-run first, capped, and audited.
These hold regardless of how a prompt is phrased, which is the point of putting the ability to spend money behind a script rather than behind an instruction:
| Dry run first | Nothing applies without an explicit flag, and the dry run validates against the live account |
| Budgets only down | A budget may be lowered, never raised. Spending less has a floor; spending more does not |
| No deletions | Campaigns and budgets can be paused but not removed. Criteria come back; a deleted campaign does not |
| New = paused | A created campaign is always paused. The agent builds it, a person decides whether it spends. There is no flag to override this |
| 20 operations | Per call, so a loop that goes wrong stays small |
| A reason, recorded | Every mutation carries a written why, into the audit bucket |
npx @meffecta/agent verify-credentials reports Ads as ok. A failure
here almost always means one of: the developer token is still test-level, the invitation was
not accepted, or an id has dashes in it.
The same identity as Ads, and the same shape: invite, do not mint.
Both authenticate as the runtime service account, so there is no token anywhere. Take its
address from status, then:
sc-domain:acme.com; a URL-prefix property is the full URL with its scheme.npx @meffecta/agent set-env ACME_GA4_PROPERTY_ID=123456789 npx @meffecta/agent set-env ACME_SEARCH_CONSOLE_PROPERTY=sc-domain:acme.com
Conversions are called keyEvents in GA4's current API — a request for a
conversions metric simply fails. Search Console data is a couple of days
behind, always; a report that treats yesterday as missing is misreading the API rather than
finding a problem.
verify-credentials reports GA4 and Search Console as ok. Both are Cloud
Run only — off it they report unavailable and the job goes on.
One OAuth client for the deployment, then one refresh token per account it acts as.
APIs & Services → OAuth consent screen. Choose External, and publish it — an app left in testing hands out refresh tokens that expire after seven days, which looks like a working set-up that mysteriously breaks a week later.
Then Credentials → Create credentials → OAuth client ID → Desktop app. Enable the Gmail, Calendar and Drive APIs while you are in the project.
npx @meffecta/agent set-secret GMAIL_CLIENT_ID
npx @meffecta/agent set-secret GMAIL_CLIENT_SECRET
npx @meffecta/agent mint-gmail --client-id <id> --client-secret <secret>
# opens a browser — sign in as the account the agent should act as
npx @meffecta/agent set-secret GMAIL_REFRESH_TOKEN
npx @meffecta/agent set-env GMAIL_ADDRESS=agent@yourdomain.com
GMAIL_ADDRESS is which mailbox that token actually is — it is what the
agent says about itself, and what a reply comes from.
A second account follows the named convention:
GMAIL_SALES_REFRESH_TOKEN and GMAIL_SALES_ADDRESS, minted with
mint-gmail --account SALES, reached by a job with
--account SALES.
Add --readonly when minting an account that should never send. The token
then has no send scope at all, which is a stronger guarantee than any instruction.
Google Workspace instead? A Workspace domain can use domain-wide delegation: grant
the runtime service account the scopes in the Workspace admin console, and the agent acts as
any user in the domain with no token to mint or rotate. Keyless, and Cloud Run only.
google-calendar and google-drive take --as user@domain
in that mode. Consumer Gmail cannot do this — OAuth is its only path.
Read-only, and normally a mailbox in someone else's tenant.
An app registration in that tenant's Entra ID, with delegated Microsoft Graph permissions Mail.Read, Calendars.Read and offline_access, and admin consent granted. Then:
npx @meffecta/agent set-env MSGRAPH_TENANT_ID=<tenant guid> npx @meffecta/agent set-secret MSGRAPH_CLIENT_ID npx @meffecta/agent set-secret MSGRAPH_CLIENT_SECRET npx @meffecta/agent mint-graph --account SUPPLIER npx @meffecta/agent set-secret MSGRAPH_SUPPLIER_REFRESH_TOKEN npx @meffecta/agent set-env MSGRAPH_SUPPLIER_ADDRESS=them@supplier.com
There is no default account here, unlike Gmail: every mailbox is named, and a job
reaches one with --account SUPPLIER. That is deliberate — these are other
people's mailboxes, and one being reachable by accident is exactly the failure to design
out.
Posting is a token. Being spoken to is a webhook job, and the two use different secrets.
A Slack app installed into the workspace, with a bot token scoped to what jobs actually
need: chat:write, reactions:write, channels:history,
app_mentions:read. Invite the bot to each channel it should post in.
npx @meffecta/agent set-secret SLACK_BOT_TOKEN # the xoxb- one
Slack gives you three seconds to acknowledge an event, and a run takes minutes. So the inbound half is an ordinary webhook job: the engine verifies the signature, acknowledges immediately, and carries the run out separately.
npx @meffecta/agent set-secret SLACK_SIGNING_SECRET
jobs/slack-mention.md
---
webhook: slack-mention
auth: slack
filter: "event.type==app_mention"
---
Then in the app's Event Subscriptions, set the Request URL to
https://<your service>/webhooks/slack-mention and subscribe to
app_mention. Slack verifies the URL by POSTing a challenge, which the engine
answers on its own — if the URL will not save, the signing secret is wrong or not yet
deployed.
SLACK_SIGNING_SECRET is not AGENT_API_SECRET and cannot be:
Slack signs with a value only Slack and the app know. It is checked inside a five-minute
window, because without one a captured signature is valid for ever and a replayed
app_mention is a job run.
An address the agent owns, rather than a person's mailbox it borrows.
Create an inbox at agentmail.to and take an API key. One key covers the account; a key scoped to a single inbox is the tighter grant where a job only ever needs one.
npx @meffecta/agent set-secret AGENTMAIL_API_KEY
Then a job that watches it — the inbox trigger:
--- inbox: agentmail:acme-agent@agentmail.to allowFrom: you@acme.com, @acme.com ---
Anyone can write to such an address and nothing filters them. That is what
allowFrom: is for, and why the engine enforces it in code rather than trusting
a prompt. A job with none refuses every message.
Polling is simple and costs one API call per check. If you want mail to arrive the instant
it lands, AgentMail can call you instead — it signs with Svix, and the
whsec_… secret is shown once, in the response that created the webhook.
npx @meffecta/agent set-secret SVIX_AGENTMAIL_SIGNING_SECRET
--- webhook: mail-arrived auth: svix:agentmail ---
A secret per webhook is normal on Svix — each job has its own route, so each has its own
webhook. auth: svix alone reads SVIX_SIGNING_SECRET;
auth: svix:<name> reads SVIX_<NAME>_SIGNING_SECRET.
Two systems where the credential is not a key you paste into the deployment.
It authenticates as the runtime service account, so what it needs is an invitation on the
project being queried. From that project, grant the address on the acts as line:
roles/bigquery.dataViewer # read the tables roles/bigquery.jobUser # run a query at all
On-demand BigQuery is billed per byte scanned, so this is the other skill with a spending cap in code: every query is dry-run first, anything over 20 GiB is refused, and the real query carries a maximum-bytes-billed ceiling anyway. Which project a job queries is a selector in the register.
A connection string is per database and carries its own credentials, so there is no shared
MONGODB_URI. Name one per project, and let the register say which is which:
npx @meffecta/agent set-secret ACME_MONGODB_URI
systems/mongodb-acme.md
---
system: MongoDB — the Acme production database
skill: query-mongodb
requires: ACME_MONGODB_URI
access: read-only — the connection string is a read-only database user
probe: list collections
---
Use a read-only database user in that string. The skill only reads, but the user is what makes that true of anything else that ever gets hold of it.
From inside the deployment, exercising each credential the way its consumer does.
npx @meffecta/agent verify-credentials
npx @meffecta/agent verify-credentials --quick # skips the slow half
Each system reports ok, FAIL or skip. A skip is not a problem — it means the variables for that one are unset, which is the normal state of most of them in most deployments. Values are never printed.
npx @meffecta/agent ask "which systems can you reach, and which not?"
That last question is the real check. verify-credentials proves the credential
works; asking the agent proves the register tells it so — and those are two different
failures with the same symptom.