meffecta agent
Reference

What it can reach,
and what that takes

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.

How access works

Every system is the same three moves in a different order.

01

Get a credential

On the other system: an API token, an OAuth refresh token, or — for Google's own marketing APIs — an invitation rather than a key.

02

Give it to the deployment

set-secret for anything secret, set-env for ids, hosts and addresses. Values never appear on screen or in your shell history.

03

Write the register entry

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.

The register entry

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.
systemWhat it is, in your words. This is what a person reads when deciding whether a job should run unattended.
skillWhich skill reaches it — a name from the catalogue. Or none, and say how it is reached instead.
requiresExact variable names, all of which must be set. none for a system that authenticates as the runtime service account.
selectorsOptional. Per-project pointers, one per world — an unset one means that project is not wired up, not that the system is broken.
accessWhat a job may do, written honestly: "read-only", "may send mail", "may change DNS".
probeThe 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.

Reading the names

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.

The catalogue

All 28, with the exact variables each reads. Set none of them and nothing breaks.

Mail, calendars and files

send-emailGmail API

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.

needs GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REFRESH_TOKEN, GMAIL_ADDRESS · walk-through
query-gmailread

Read the mailbox the agent serves — search inbox and sent mail, read a body, list drafts, list a day's calendar events.

needs the same GMAIL_* set. A second account follows GMAIL_<NAME>_REFRESH_TOKEN / _ADDRESS, chosen with --account NAME
google-calendar

Create and update calendar events — in a Workspace calendar via domain-wide delegation, or a private one via OAuth.

needs the GMAIL_* OAuth set, or domain-wide delegation on the runtime service account
google-drive

Read and write Drive, Sheets, Docs and Slides — search, read, export as text, create documents, append rows.

needs the GMAIL_* OAuth set, or domain-wide delegation
query-outlookread

Read a Microsoft 365 mailbox and calendar through Graph. Read-only by design — it cannot send.

needs MSGRAPH_TENANT_ID, MSGRAPH_CLIENT_ID, MSGRAPH_CLIENT_SECRET, and MSGRAPH_<NAME>_REFRESH_TOKEN per mailbox · walk-through
agentmail

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.

needs AGENTMAIL_API_KEY · walk-through

Analytics, search and the market

query-google-marketingread

Search Console, Google Analytics 4 and Google Ads performance in one skill — which queries bring clicks, where traffic came from, what a campaign spent.

needs no key: it authenticates as the runtime service account. Per project: <WORLD>_GA4_PROPERTY_ID, <WORLD>_SEARCH_CONSOLE_PROPERTY, <WORLD>_GOOGLE_ADS_CUSTOMER_ID, plus GOOGLE_ADS_DEVELOPER_TOKEN and GOOGLE_ADS_MANAGER_CUSTOMER_ID · walk-through
query-posthogread

Production product analytics — did anyone do X, how many, when, funnels and adoption, and what a capture actually contains.

needs POSTHOG_HOST, POSTHOG_API_KEY, and <WORLD>_POSTHOG_PROJECT_ID per project
query-serpread

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.

needs SERPER_API_KEY · 2,500 queries free, then paid credits; every successful call spends one
query-ahrefsread

A domain's Ahrefs Domain Rating and the Ahrefs crawler IP list, via the free public API.

needs AHREFS_API_KEY
seo-geo-aeoprocedure

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.

needs nothing. It fetches public pages, and uses whichever of the skills above you have

Logs, warehouses and databases

query-grafanaread

Production logs from Grafana Cloud (Loki) — the server side of an incident, traced by id.

needs GRAFANA_LOGS_HOST, GRAFANA_LOGS_USERNAME (the numeric Loki user id, not an email), GRAFANA_API_KEY with logs:read
query-coralogixread

Coralogix logs with DataPrime or Lucene — filter by application, subsystem, severity and time, and aggregate.

needs CORALOGIX_DOMAIN, CORALOGIX_API_KEY with the DataQuerying preset. The domain is region-specific and there is no global one — eu2, us1 and ap1 are separate installations and a key from one does not work in another
query-elasticsearchread

Full-text and filtered search over indices, aggregations, and the mappings that say what a field actually is.

needs ELASTIC_URL, ELASTIC_API_KEY — the 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 rest
query-bigqueryread

SQL against a BigQuery warehouse, plus the dataset, table and schema inspection to write it correctly.

needs no key — it authenticates as the runtime service account, so it works on Cloud Run and nowhere else. What it needs is an IAM grant · walk-through
query-mongodbread

List collections, inspect a collection's real shape, find and filter documents, and aggregate for counts, groupings and time series.

needs a connection string per database. Not a shared variable — it is a per-project selector you name in the register · more

Records and revenue

query-hubspotread

Contacts, companies, deals, tickets, notes and calls, plus owners, pipelines and the custom properties a portal has defined.

needs HUBSPOT_TOKEN — a private-app access token, one app per portal. A second portal follows HUBSPOT_<NAME>_TOKEN
manage-hubspotwrites

Create or update a contact, company or deal, move a deal to another stage, log a note or a call, associate records.

needs the same token, with write scopes on the private app. Give it read-only scopes and this skill cannot work regardless of what a prompt says
query-kleerread

Kleer accounting and payroll (the Swedish Finance-as-a-Service platform) — client invoices and payment status, supplier invoices awaiting certification, the ledger.

needs KLEER_API_TOKEN — issued by a Kleer consultant and bound to a user whose permissions it inherits. Point it at a read-only user; that is what actually stops a write

Where the work happens

slack

Post a message or a threaded reply, add a reaction, read a channel's history or a thread, look up a user or channel.

needs SLACK_BOT_TOKEN, and SLACK_SIGNING_SECRET for the inbound half · walk-through
linear

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.

needs LINEAR_API_KEY. Personal keys go in the header raw, with no Bearer — unlike every other credential here, which is the usual reason a first call 401s. A second workspace follows LINEAR_<NAME>_KEY
webflow

Read and change a Webflow site — pages, CMS collections and their items — and publish it.

needs WEBFLOW_TOKEN, scoped per site and per permission: 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 prompt
mevisio

The 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.

needs MEVISIO_HOST, MEVISIO_EMAIL, MEVISIO_PASSWORD. The platform is multi-tenant by hostname, so the host is not a setting — it is whose boards these credentials open

Infrastructure and the open web

cloudflare

Zones, DNS records, cache purge, Pages projects and deployments, certificate checks.

needs CLOUDFLARE_API_KEY — an API token despite the name, not the Global API Key — and CLOUDFLARE_ACCOUNT_ID. Scope the token to the zones it should touch
browser

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.

needs nothing of its own. Credentials for a particular site are that system's register entry, and sessions are kept under the memory directory

Spending money

manage-google-adswrites

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.

needs the same access as the read half, with the invitation raised from read-only · walk-through

Procedures and the agent's own

iso-27001procedure

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.

needs nothing. Two rules carry it, and they are the same rule: never record that a control is in place, and never write more system than the business can sustain
spawn-job

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.

needs nothing. Every job has it, and it buys time rather than permission · how it is bounded

Per-project selectors

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 plumbingAGENT_*, GIT_REPO_URL, GITHUB_TOKEN, CLAUDE_CODE_OAUTH_TOKEN. The runner itself, validated at boot.
Shared loginsOne credential covering many projects: GRAFANA_*, POSTHOG_*, GMAIL_*, CLOUDFLARE_*, GOOGLE_ADS_DEVELOPER_TOKEN. No prefix.
SelectorsWhich 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.

GA4 and Search Console

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:

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.

Gmail, Calendar and Drive

One OAuth client for the deployment, then one refresh token per account it acts as.

01

An OAuth client, in the agent's own project

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.

02

A refresh token per account

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.

03

More accounts, and read-only ones

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.

Microsoft 365

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.

Slack, both directions

Posting is a token. Being spoken to is a webhook job, and the two use different secrets.

Outbound — the agent posts

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

Inbound — somebody mentions it

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.

AgentMail, and webhooks signed by Svix

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.

The webhook alternative

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.

BigQuery, MongoDB, and things with no shared variable

Two systems where the credential is not a key you paste into the deployment.

BigQuery — a grant, not a key

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.

MongoDB — per database, 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.

Checking it worked

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.

Nothing reports FAIL, and the systems you just connected report ok. Then add a register entry for each, and ask the agent what it can reach: 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.

Next