Build and customize GTM OS by talking to Claude
This guide is for the person who sets up GTM OS for a company and keeps tuning it. You work with Claude Code or Claude Desktop, the gtm command line and the console. You change engines and business logic by writing prompts. Two people approve what goes live.
How each chapter works
- Goal says what you will be able to do.
- Prompts are in dark boxes with a Copy button. Replace anything in
<angle brackets>. - Claude does names the operations Claude calls. You do says what a person must do.
- Needs a human marks a step Claude cannot do for you.
- You should see tells you how to check that it worked.
Rules that explain most of what you will see
- Claude proposes. A different person approves. The person who proposes can never approve, sign or execute. You need a second approver from day one.
- Dev first, then prod. Definitions Claude writes land in
dev. They go live only after a person signs them and promotes them toprod. - Dry first, then live. A new organization starts with every engine in Dry Mode. You switch an engine live on purpose, in Settings > Safety.
- Definitions are the business logic. Scoring, routing, sequences and voice are signed, versioned documents, not hidden settings.
Where this guide fits
The user guide walks an operator through the console. The product documentation explains the architecture for evaluators. This guide goes deeper for the engineer who builds and customizes. Your project also holds the same material for Claude in docs/gtmos/ (refresh it with gtm init --refresh). When a page and your live instance disagree, the instance wins.
| Part | Chapters |
|---|---|
| Start | 1 Mental model, 2 Install and connect, 3 MCP tools, 4 Identity and keys |
| Build | 5 Definitions as code, 6 Build by prompting, 7 Business logic |
| Engines | 8 How engines work together, 9 Engine by engine |
| Data, agents, extend | 10 Data in and out, 11 Agents and missions, 13 Apps and packages |
| Govern and operate | 12 Governance, 14 Operate the instance |
| Support | 15 Recipes, 16 Troubleshooting, 17 Reference |
The mental model for a GTM engineer
You build your go-to-market system by talking to Claude. Claude works through one door, the GTM OS MCP server. Everything it does is an operation. Anything that goes live or touches a customer waits for a second person to approve it.
The model in one minute
Goal: know what happens between "you ask" and "it is live", so you can predict what Claude will do and where it will stop.
- You say what you want in plain words.
- Claude looks up the exact operation (with the
gtm_describetool) and calls it with its own agent key. The key acts for you but is never you. - Reads and internal changes run at once. A change that goes live, or anything aimed at a customer (a send, a CRM write, a merge, a page change, an ad budget change), becomes a proposal.
- A different person with the owner or admin role approves the proposal in the console (Approvals), then executes it. Needs a human
- GTM OS writes down who proposed, who approved and what ran. You can read that trail in Settings > Audit log.
Claude never approves anything, and neither can you for what your own agent proposed. This rule is called four-eyes. It is the reason you need a second approver from day one (see Approvals and four-eyes).
The pieces you will touch
| Piece | What it is | Where it comes up in this guide |
|---|---|---|
| Operations | The single list of things GTM OS can do. The console, the REST API, the CLI, the SDK and MCP all call the same list, so no door skips the rules. | Chapter 3 |
| Ontology | The record types, for example gtm.account, gtm.contact, gtm.deal, gtm.signal and gtm.activity, plus your own custom fields and custom objects. | Build chapters |
| Engines | The packaged workers that score, enrich, listen, write, rank and publish. They read your definitions. | Below |
| Definitions | Versioned configuration the engines read, such as a scoring model, a lane or a play. They are written in dev, signed by a person, then promoted to prod. | Build chapters |
| Governance | Proposals, approvals, environments, agent identities, Dry Mode, the audit log. | Chapter 4 |
| Blueprints and packages | A blueprint seeds a whole motion in one go. Packages install engines, screens, connectors and modules. | Build chapters |
The five operation classes
Goal: read the class letter of an operation and know what a call will do.
| Class | Meaning | What happens when Claude calls it |
|---|---|---|
| Q | Query, a read | Runs at once. Nothing changes. |
| D | Data write | Runs at once. Changes records or settings inside your organization. |
| T | Stage transition | Runs at once. Moves a record to its next stage. |
| E | Customer-facing effect | Becomes a proposal. Nothing is sent or written outside until a different person approves and executes it. |
| M | Meta: configuration, ontology, platform | Depends on the operation. Publishing config, promoting to prod and installing packages go through the same approval gate. |
The 0.2.5 catalog has 671 operations: 294 Q, 255 D, 5 T, 30 E and 87 M. You do not memorize them. Claude lists them with gtm_describe, and your project holds the full generated list at docs/gtmos/reference/operations.md after gtm init.
What ships in 0.2.5, and what does not
Goal: know which engines exist today, so you never ask Claude to configure something that is not there.
Seven engines ship as packages. Each runs on your signed definitions and falls back to a built-in default when you have none.
| Engine | What it does | Where you see it |
|---|---|---|
| Score | Scores accounts from signals and firmographic fit, with the reasons behind each verdict. | Engines > Score; CRM > Hot list |
| Enrichment | Fills empty fields from providers, stamps freshness, re-checks stale values. Spend is real, so it always quotes first. | Engines > Enrichment |
| Signals | Ingests buying, growth and risk signals and weighs them by age. | CRM > Signals; CRM > Hot list |
| Outreach | Drafts, personalizes, sequences and sends. Every send is an approval-gated effect through an active lane. | Engines > Outreach; CRM > Sequences |
| SEO & GEO | Audits tracked pages, recommends changes, tracks rank and AI-search readiness, runs the content factory, publishes through an approval. | Engines > SEO & GEO |
| Performance Ads | Syncs spend, paces against your CAC target, proposes budget and status changes. This is real money. | Engines > Performance Ads |
| Identity & data quality | Finds duplicates, validates and normalizes, and runs gated, reversible merges. | Engines > Merges (Data quality tab); Operate > Monitors |
Around the engines sit things that are not packaged engines but have their own pages and operations: plays (Engines > Plays), routing (Engines > Routing), the research agent (CRM > Research), CRM sync, source mappings, the adapter builder (CRM > Adapters) and AI chat.
Four safety rules that explain most of what you will see
- Four-eyes. Whoever proposes a change can never approve it. For an agent's proposal, the person the agent acts for cannot approve it either. A second owner or admin must.
- Dev first, then prod. Every organization has
dev,stagingandprod.prodis the default: a call that names no environment runs inprod. Definitions cannot be published straight intoprod. They are written indev, signed by a person, then promoted. Customer-facing effects indevandstagingare recorded but not sent. - Dry first, then live. Dry Mode (Settings > Safety) turns every customer-facing action into a draft across the whole organization. Each engine also has its own mode,
dryorlive, and in a new organization every engine startsdryuntil a person flips it. - Never fake data. A value with no source is shown as unverified. Fixture data in a demo instance is marked as fixture. Claude must tell you when a check is not green.
The working loop
Goal: one loop you repeat for every change, whatever the engine.
- Describe. Claude reads the operation or definition kind it is about to use.
- Change in dev. Claude writes the change to
devand says so. - Propose. Claude reports the approval id and stops. Needs a human
- Approve and sign. A second person approves, then signs the definition. Needs a human
- Promote. One approval promotes everything from
devtoprod(CRM > Environments, the Go live card). Needs a human - Verify. Claude reads the result back and shows you the proof, for example a record, a score reason or a readiness line.
The build chapters go through each step in detail. For now, remember the sentence Claude should end with after any definition change: "I wrote X in dev. It is not live until it is signed and promoted."
Where to look in the console
Console paths are written as Group > Page. Every page lives under /org/<orgId>/.
| You want to see | Open |
|---|---|
| What is waiting for a decision, and its history | Approvals (top of the sidebar) |
| Only what is waiting on a person right now | Operate > Needs a human |
| What Claude wrote and who ran it | Settings > Audit log |
| What is in dev and not yet live; sign and promote | CRM > Environments |
| Dry Mode | Settings > Safety |
| Your agent keys and the connect snippets | Settings > Developer |
| Stop an agent, or bring it back | Settings > Agents |
| Provider keys and connected apps | Integrations > Connections |
| Runs and their step-by-step traces | Operate > Runs |
First prompt to try
Explain what you can and cannot do on this instance
Call agent.whoami, policy.get_dry_mode and instance.readiness. Then tell me in plain words: who you are acting as, which scopes your key has, whether Dry Mode is on, which providers are live, and what you cannot do without a person.- Claude does
- Runs three read operations through
gtm_query. None of them changes anything. - You do
- Nothing. Read the answer. If a provider you need is not live, fix it in Integrations > Connections.
record.read), "cannot approve", the Dry Mode state and one line per provider. A read-only key is not allowed to read everything (for example env.list needs engine.configure), and Claude should say so rather than guess.Install and connect Claude
Two jobs: put a GTM OS instance on your machine, and connect Claude to it with its own key. Claude Code does most of the work for you. Claude Desktop can connect too, with limits.
Check the prerequisites
Goal: a machine that can run the stack and a working gtm command.
- Docker Desktop, installed and running.
- 10 GB or more of free disk (images plus database).
- 4 GB or more of free RAM for the stack (Postgres, API, worker, MCP, console). On Windows, Docker runs in WSL 2. If containers are killed, raise the memory limit in
%UserProfile%\.wslconfigand restart Docker. - Node 22 or newer.
- Claude Code.
- The gtm CLI from npm:
npm i -g @gtmos/clidocker info --format '{{.ServerVersion}}'
node -v
gtm version --offlinev22.x or newer, and the gtm CLI version.Make a workspace and run gtm init
Goal: an empty folder that Claude Code can work in, loaded with GTM OS skills, slash commands and the MCP entry.
- Make a folder and start Claude Code in it:
mkdir my-gtm && cd my-gtm claude - Inside Claude Code, the ! prefix runs a shell command. Run:
! gtm init - Quit Claude Code and start it again in the same folder. Claude reads new slash commands and skills only at startup.
CLAUDE.md, AGENTS.md, .mcp.json, .claude/ (subagents, slash commands, skills, memory seeds, settings) and docs/gtmos/ (guides and a generated reference).With no instance yet, gtm init never fails. It scaffolds offline and tells you what to do next. If an instance already answers on this machine at http://127.0.0.1:3001, it says so.
.mcp.json holds Bearer ${GTMOS_AGENT_TOKEN}, a reference to an environment variable, not the key. If gtm init finds a literal key in .mcp.json it rewrites the entry, warns you, and you should revoke that key.Let Claude install the instance: /setup-gtmos
Goal: a running instance, an owner, a second approver and a connected agent key, with Claude doing the typing.
/setup-gtmosClaude checks the prerequisites, then asks you one grouped question: use an instance already running here or install a new one; organization name; owner email (you); approver email (a different person); version (stable or a local image tag); ports; and which provider keys you have ready.
| Phase | Claude does | You do |
|---|---|---|
| Install | Runs gtm setup ... --no-harness into ./instance. A few minutes. Safe to re-run. | Nothing. |
| Passwords | Tells you the owner and approver passwords are in instance/.env as GTMOS_OWNER_PASSWORD and GTMOS_APPROVER_PASSWORD. It never prints them. | Give the approver password to the approver privately. |
| Agent key | Signs in as the owner, creates an agent key, runs gtm init --url ... --key ... and gtm doctor. | Set GTMOS_AGENT_TOKEN and restart Claude (see Put the key in your environment). |
| Connections | Stores and tests the provider keys you give it and links your apps. | Paste keys in Integrations > Connections, and click any OAuth consent. Needs a human |
/mcp lists gtmos as connected. Claude then hands off to /build-gtm-system (see the build chapters).Prefer to see every step yourself? The next section does the same by hand.
The same thing by hand
Goal: install an instance and connect a workspace without /setup-gtmos. Commands are for bash. On PowerShell, write $env:NAME = "value" instead of export NAME=value.
- Install into its own folder. The owner and approver passwords are generated into its
.env:Port busy? Addgtm setup gtm-instance --org "Acme" --email you@acme.example --approver-email cofounder@acme.example--console-port,--api-port,--mcp-portor--postgres-port. Local images only? Add--version <tag> --no-pull. - Sign in as the owner. It asks for the password, or reads
GTMOS_PASSWORD. Your org id isORGANIZATION_IDingtm-instance/.env:Run theexport GTMOS_API_URL=http://127.0.0.1:3001 GTMOS_ORG=<org id> gtm login --email you@acme.example --console-url http://localhost:3000export GTMOS_TOKEN=...line it prints. This is your owner session. It is not the agent key. - Create the agent key. It is shown once:
gtm token create --name claude-code --client claude-code --preset full-gtm-engineer --expires-days 90 - Put the key in your environment (next section).
- Connect the workspace and check it:
gtm init --url http://127.0.0.1:3001 --key <agent key> --yes gtm doctor - Start
claudein the workspace and run/mcp.
gtm init prints numbered steps. Step 2 reads like key OK: agent "claude-code" · org <id> · scopes record.read, record.write, engine.configure, connection.manage · cannot approve. The last block says Readiness: GO or NO-GO with one line per blocker. gtm doctor prints one line per provider with the tag LIVE, UNVERIF, FAILED, FIXTURE or MISSING and the fix for each.Exit codes of gtm init: 0 done, 1 usage or file error, 2 instance or key problem, 3 refused key. It refuses a credential that can approve (a person's session), because a coding agent must only propose.
Put the key in your environment
Goal: Claude Code finds the key through GTMOS_AGENT_TOKEN and the key never lands in a file you might commit.
Windows PowerShell (persistent, then open a new terminal):
[Environment]::SetEnvironmentVariable('GTMOS_AGENT_TOKEN', '<agent key>', 'User')macOS or Linux: add this line to ~/.zshrc or ~/.bashrc, then open a new terminal:
export GTMOS_AGENT_TOKEN='<agent key>'The MCP address defaults to http://localhost:3004/mcp. To point at another address, set GTMOS_MCP_URL. .mcp.json reads it as ${GTMOS_MCP_URL:-http://localhost:3004/mcp}.
echo $GTMOS_AGENT_TOKEN (or $env:GTMOS_AGENT_TOKEN on PowerShell) prints something that starts with gtm_. Do not paste that output anywhere.Connect Claude Code
Goal: the gtmos MCP server shows as connected, with all nine tools.
- In Settings > Developer, the Connect Claude Code / Codex card creates the same key as
gtm token create. Choose the coding agent, an agent name and the preset, then Create agent key. Copy the key now. The page also shows your org id, the MCP address and copy-ready snippets. Needs a human - Set
GTMOS_AGENT_TOKENas above, then in the workspace folder rungtm init --url <api url> --key <agent key> --yes. - Restart Claude Code in the folder and run
/mcp.
/mcp lists gtmos as connected. Ask "What GTM OS tools do you have?" and Claude names gtm_describe, gtm_query, gtm_get, gtm_search, gtm_act, gtm_approvals, gtm_lineage, gtm_lineage_trace and search_records.The .mcp.json that gtm init writes has this shape (the URL is your instance's MCP address):
{
"mcpServers": {
"gtmos": {
"type": "http",
"url": "${GTMOS_MCP_URL:-http://localhost:3004/mcp}",
"headers": { "Authorization": "Bearer ${GTMOS_AGENT_TOKEN}" }
}
}
}To make every MCP call in this project default to dev, run gtm init --env dev. It adds an x-gtmos-environment header to that entry. Without it, calls that name no environment run in prod.
Quick check that the key works, outside Claude (bash):
curl -s -X POST "http://localhost:3004/mcp" \
-H "Authorization: Bearer $GTMOS_AGENT_TOKEN" \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Keep the project current after you upgrade the CLI or the instance. Files you edited are kept:
gtm init --refresh --yesCommon errors
UNAUTHORIZED"No valid session or agent token": the variable is missing, wrong, revoked or expired (keys last 90 days by default). Create a new key, set it, restart.- "No commands match /setup-gtmos": Claude Code was already running when
gtm initwrote the commands. Quit and start it again in that folder. - MCP tools fail after you restart or upgrade the stack: run
/mcpto reconnect. gtm initsays the API is not reachable and shows an HTML-page hint: you passed the console address. Use the API address (defaulthttp://127.0.0.1:3001).- HTTP 403 on every call: the agent was killed, or the key belongs to another organization. A person revives the agent in Settings > Agents.
gtm initexits with code 3, "REFUSED": you gave it a person's credential. Use an agent key.
Connect Claude Desktop
Goal: ask questions of your instance from Claude Desktop. Know first what you give up.
gtm init, /setup-gtmos, the slash commands, the skills and the subagents are made for Claude Code and Codex. Claude Desktop gets the same nine tools, but none of that scaffolding. Use it to read, ask and review, not to build.GTM OS speaks streamable HTTP and wants an Authorization: Bearer header. Claude Desktop starts local programs from its config file. To bridge the two you run a small helper in between. The steps below use mcp-remote.
Untested path. The bridge steps below were not run against a GTM OS instance, and the mcp-remote flags may differ in its current release. Whether a Claude Desktop custom connector (Settings > Connectors) can reach a GTM OS MCP server is not confirmed. If the bridge does not connect, use Claude Code.
- Create a key for it in Settings > Developer. Choose Other MCP client, give it its own name (for example
claude-desktop-anna), and pick theread-onlypreset unless you need more. Use a short expiry. Needs a human - In Claude Desktop open the Claude menu, then Settings, then Developer, then Edit Config. This opens
claude_desktop_config.json. Its usual location is%APPDATA%\Claude\claude_desktop_config.jsonon Windows and~/Library/Application Support/Claude/claude_desktop_config.jsonon macOS. - Add the server. Node must be installed, because
npxruns the bridge:{ "mcpServers": { "gtmos": { "command": "npx", "args": ["-y", "mcp-remote", "http://127.0.0.1:3004/mcp", "--header", "Authorization:${GTMOS_AUTH}"], "env": { "GTMOS_AUTH": "Bearer <agent key>" } } } } - Quit Claude Desktop completely and start it again.
- In a new chat open the Add files, connectors, and more menu, then Connectors, and check that gtmos is listed.
The cost of this setup: the agent key is written into the config file. Treat that file like a password. Never commit it, never share it, use a read-only or short-lived key, and revoke the key in Settings > Developer when you stop using Desktop. .mcp.json avoids this for Claude Code because it only references an environment variable.
If it does not show up
- Check the JSON is valid and that you fully quit Desktop before reopening.
- Read the logs:
%APPDATA%\Claude\logs\mcp.logon Windows,~/Library/Logs/Claude/mcp.logon macOS. A 401 means the key is wrong, expired or revoked. - Connection refused: the instance is not running, or you used another port. Check
http://127.0.0.1:3004/health(it answers{"status":"ok", ...}).
Prompts for this chapter
Install GTM OS and connect yourself
/setup-gtmos- Claude does
- Runs the setup runbook: prerequisites, install into
./instance, agent key,gtm init,gtm doctor, then asks you to restart. - You do
- Answer the grouped question, paste provider keys in the console, set
GTMOS_AGENT_TOKEN, restart Claude Code.
Check my connection and my key's scopes
Check that the gtmos MCP connection works. Call agent.whoami and show me my agent name, my scopes and whether I can approve. Then run gtm doctor and summarize anything that is not LIVE. Do not change anything.- Claude does
- Reads
agent.whoamithroughgtm_query, runsgtm doctorin the shell. - You do
- Fix what is not live, in Integrations > Connections.
The MCP toolbox in practice
Claude reaches all of GTM OS through nine tools. Eight are generic and cover every operation. One is a search tool for plain-language questions. Learn what each one does and you can read what Claude is doing.
The nine tools at a glance
| Tool | Use it to | Changes data? |
|---|---|---|
gtm_describe | List every operation, or read one operation's exact input and output shape. Always first. | No |
gtm_query | Run a Q-class operation (a read) by name. | No |
gtm_get | Read one record by object type and id. | No |
gtm_search | List records of one object type, newest first, at most 50. | No |
search_records | Plain-language search across record types, ranked by relevance. Returns short previews. | No |
gtm_act | Run a D, T, E or M operation by name. An E operation becomes a proposal. | Yes |
gtm_approvals | Read the approval queue, count it, or read one approval. | No |
gtm_lineage | Read the audit trail behind one approval: who proposed, who approved, what ran. | No |
gtm_lineage_trace | Read the receipts behind a record's values: the winning value, the rule that chose it, the sources. | No |
Every call goes through the same path as the console and the REST API. Claude has no side door. Everything the console does is an operation, so what you can click, Claude can do, minus the things only a person may do (see What only a person can do).
gtm_describe: look before you call
Goal: get an operation's exact input shape so no field name is guessed.
Arguments: name (an operation name, optional) and class (Q, D, T, E or M, optional).
gtm_describe {} the whole catalog
gtm_describe { "class": "E" } only customer-facing effects
gtm_describe { "name": "records.search" } one operation in fullWith no name you get one row per operation: name, class, method, summary, required scopes, error codes. With a name you also get the version, whether it is idempotent, whether it supports a dry run, and the input and output as JSON Schema.
records.search: class Q, required objectType, optional filter and limit (1 to 50).When a guide and the live answer from gtm_describe disagree, the live answer wins.
gtm_query: run a read
Arguments: name (required), input (an object, optional), environment (optional). It refuses write operations and tells Claude to use gtm_act.
gtm_query { "name": "agent.whoami" }
gtm_query { "name": "policy.get_dry_mode" }
gtm_query { "name": "instance.readiness", "input": { "status": "not_configured" } }
gtm_query { "name": "runtime.list_approvals", "input": { "state": "proposed", "limit": 20 } }agent.whoami returns your organization id, the principal, how it signed in, the agent's name and version, the role, the effective scopes and canApprove (false for any agent). policy.get_dry_mode returns { "enabled": true } or false.gtm_get, gtm_search and search_records: find records
| Tool | Arguments | Behavior |
|---|---|---|
gtm_get | objectType, id, environment | One record, canon or custom. |
gtm_search | objectType, limit (1 to 50, default 10), environment | A capped list, newest first. Never a bulk dump. |
search_records | query, objectType (optional), limit (1 to 25, default 5) | Full-text plus semantic ranking fused into one score. Short previews only. Use gtm_get for the full record. If no embedding provider is configured, the answer says mode: "text_only". That is not an error. |
gtm_search { "objectType": "gtm.account", "limit": 10 }
gtm_get { "objectType": "gtm.account", "id": "<account id>" }
search_records { "query": "fintech accounts that hired a head of sales", "objectType": "gtm.account", "limit": 5 }Record visibility applies. A private or team record is returned only to its owner, its creator and the organization's owners and admins. An agent sees what the person it acts for sees.
gtm_act: change something
Arguments: name (required), input, environment. It refuses read operations and tells Claude to use gtm_query.
gtm_act { "name": "score.score_account", "input": { "accountId": "<account id>" } }score.score_account is a D operation. It runs now and writes a new qualification with its reasons.
gtm_act { "name": "outreach.send_message", "input": { "messageId": "<message id>" } }outreach.send_message is an E operation. This call proposes the send. Nothing is sent. The result carries the approval, and a different person must approve and execute it. A send that has no lane fails with lane_required. While Dry Mode is on, E operations return drafts and create no approval at all.
gtm_approvals.gtm_approvals and gtm_lineage: follow a proposal
| Call | Returns |
|---|---|
gtm_approvals {} | The queue, newest first. |
gtm_approvals { "state": "proposed", "engine": "outreach", "limit": 20 } | Only what waits on a person, only for one engine. |
gtm_approvals { "count": true } | Counts per engine and a total. By default it counts what waits on a person. |
gtm_approvals { "id": "<approval id>" } | One approval with its history. |
gtm_lineage { "approvalId": "<approval id>" } | The trail, oldest first: propose, approve or reject, begin and end of execution. |
Engine keys you can filter by: outreach, meetings, perf_ads, seo_geo, routing, crm_writeback, enrichment and identity_dq. The states are proposed, approved, rejected, expired, revoked, withdrawn, executing, executed and failed. A proposal expires after 7 days.
gtm_lineage_trace: where did this value come from
gtm_lineage_trace { "recordId": "<account id>", "objectType": "gtm.account", "property": "employee_count" }For each property you get the winning value, the rule that chose it, a confidence, and the observations that fed it with their sources. A record with no provenance is labeled unverified, never invented. In 0.2.5, lineage for derived values and metrics is not filled in yet and comes back empty.
Which environment does a call use
gtm_query, gtm_get, gtm_search and gtm_act take an optional environment (dev, staging, prod or a named one). If you leave it out, the call uses the x-gtmos-environment header from .mcp.json when there is one, and otherwise prod. An unknown environment is refused with VALIDATION_FAILED. It never silently falls back to prod.
dev is not live. Runtime reads prod. A scoring model you change in dev changes nothing for real accounts until it is signed and promoted. Ask Claude to say which environment it read and which it wrote, every time.Read the error, then the remediation
Errors come back as a normal tool result flagged as an error, with this shape:
{ "error": { "code": "FORBIDDEN", "message": "...", "remediation": "...", "details": { } } }The remediation text usually names the exact fix. Tell Claude to read it before it retries.
| Code | Meaning |
|---|---|
VALIDATION_FAILED | The input does not match the schema (see details.issues), the tool and class do not match, or a named thing does not exist. |
UNAUTHORIZED | No valid key. Missing, wrong, revoked or expired. |
FORBIDDEN | A role, a scope, a human-only rule or four-eyes refused it. |
NOT_FOUND | That id or operation does not exist in this organization. |
NOT_CONFIGURED, CONFLICT | A provider or connection is missing, or the thing is in the wrong state. |
APPROVAL_REQUIRED | Not a failure. The action is waiting for a person. |
RATE_LIMITED | Too many calls. Wait, then retry. |
Claude must tell you at once when a call returns FORBIDDEN, NOT_CONFIGURED or VALIDATION_FAILED: the operation, the error, and the fix. This is built into the project's CLAUDE.md.
Rate limits apply per signed-in session or key, per anonymous address and per IP address. They are set in the instance .env with GTMOS_RATE_LIMIT_API_PER_MIN, GTMOS_RATE_LIMIT_API_ANON_PER_MIN and GTMOS_RATE_LIMIT_API_IP_PER_MIN (see the config keys). Read the shipped values in the .env.example of your release.
Prompts for this chapter
Describe an operation, then call it
Use gtm_describe on score.score_account and show me its input. Then pick one account from gtm_search on gtm.account, score it with gtm_act, and show me the reasons it gave. Tell me which environment you used.- Claude does
- Reads the schema, lists accounts, runs the D operation, reads the result.
- You do
- Nothing. It writes one qualification row. Run it only on a test account if you care.
Show what is waiting for approval and why
Use gtm_approvals with count true, then list everything in state proposed. For each one tell me what it will do, who proposed it, which engine owns it and who can approve it. Do not try to approve anything.- Claude does
- Counts, lists, then reads each one with
gtm_approvalsandgtm_lineage. - You do
- Hand the list to the approver. Needs a human
Where did this value come from
For the account <account id>, use gtm_lineage_trace and explain in plain words where its employee count came from and which rule picked it.- Claude does
- Calls
lineage.tracethroughgtm_lineage_trace. - You do
- Nothing.
Identity, scopes, keys and approvals
Claude acts as a named agent with its own key. What it can do is the overlap of three things: the key's scopes, the agent's scopes and the current role of the person who made the key. A short list of actions stays human, always.
Who Claude is when it calls GTM OS
Goal: understand the identity behind every MCP call, so the audit log and the approval rules make sense.
- An agent key belongs to a named agent in your organization. The agent acts on behalf of the person who created the key.
- Every action is attributed to the agent and the key in the audit log, not to you.
- An agent can propose. It can never approve, reject or execute an approval, and a key can never be granted the right to approve.
- Keys follow the person's current role. Demote or remove the person and their agents narrow or stop at once.
agent.whoami shows it all: agent name, role, effective scopes and canApprove: false.Scopes and presets
| Scope | Lets the agent |
|---|---|
record.read | Read. Always included. |
record.write | Write records and propose customer-facing effects. |
engine.configure | Change configuration: definitions (as proposals), environments, settings, workflow flags, research settings. Called the build scope. |
connection.manage | Store and test connections and link apps. |
action.approve is a scope too, but it can never be put on a key. The command refuses it, and so does the database.
| Preset | Scopes | Default for |
|---|---|---|
full-gtm-engineer | All four | Claude Code |
read-write | record.read, record.write | |
read-only | record.read | |
| (none given) | record.read, record.write, engine.configure | Codex and other clients |
In the generated operation reference, each operation lists who can call it. operate means record.read for reads and record.write for writes. build means engine.configure, and the person behind the key must be an owner, an admin or a builder. admin means the right to approve, and an agent never has it.
Effective authority: three things must agree
What the agent can do = the key's scopes, intersected with the agent's scopes, intersected with the current role of the person who made the key. It is checked on every call.
| Situation | Result |
|---|---|
The key has engine.configure, but a member (not an owner or admin) created it | Build operations refuse. The message says the agent acts for a member. |
| The person who made the key is demoted | The key shrinks with them at the next call. |
The key is a read-only preset | Reads that need only record.read work. Writes and build reads (such as env.list) refuse. |
agent.whoami and read scopes. That list is what your key really carries on this request, not what you asked for when you made it.Create, list and revoke a key
Goal: a key with the scopes you chose, shown once, that you can revoke. Needs a human Claude cannot create keys. Key management needs a signed-in person.
- Console: Settings > Developer, Connect Claude Code / Codex. Choose the client, name, preset and expiry, then Create agent key. Copy the key now.
- Terminal: sign in first, then create (your owner or admin session, not an agent key):
gtm login --email you@acme.example gtm token create --name claude-code-anna --client claude-code --scopes record.write,engine.configure --expires-days 90--clientisclaude-code,codexorother. Use--preset full-gtm-engineer|read-write|read-onlyor--scopes, not both.--expires-daysis 1 to 365 ornever, and the default is 90. - List keys (it never shows key material) and revoke one:
gtm token list gtm token revoke <key-id>
gtm token create prints the key once, with bash and PowerShell lines for GTMOS_AGENT_TOKEN, GTMOS_MCP_URL and GTMOS_ORG. gtm token list prints one line per key: id, agent and its status, scopes, last use, expiry.Common errors
- "GTMOS_TOKEN holds an API key. Keys cannot mint keys": use
gtm loginand its session token. - "GTMOS_TOKEN is not set": run
gtm login --email <owner email>and paste the export line it prints. - "action.approve can never be granted to a key": expected. A person approves in the console.
- Key expired or revoked: the call returns 401 with
UNAUTHORIZED. There is no separate "expired" code. Make a new key.
What only a person can do
Goal: know in advance which requests Claude must hand back to you.
Claude gets FORBIDDEN "Operation ... can only be performed by a human" on these, whatever its scopes.
| Why | What |
|---|---|
| Four-eyes | Approve, reject or execute an approval; sign a definition; publish an ontology change; approve a delegation or a mission; decide a research fact. |
| No self-escalation | Create or list keys, register, revive or kill agents, set gate policy or autonomy scope, grant envelopes, mark MCP tool effects, SSO and sign-in allowlist settings. |
| Browser consent | OAuth sign-ins (Composio, Google, Microsoft, Slack). Claude gives you the link. |
| Safety switch | Turn Dry Mode on or off. Claude can read it. |
| Irreversible | Offboard an organization. Launch the setup build. |
Claude also cannot switch on research auto-apply, because that removes the human review of research suggestions.
What Claude can do with the Full GTM engineer preset, acting for an owner or admin: records, custom fields, pipelines, definitions (as proposals), research profile and settings, CRM settings, workflow flags, connections and keys you give it, MCP servers, syncs, scans, blueprints and proposals. High-impact changes come back as proposals.
The full list of operations that refuse an agent key is in the reference.
Approvals and four-eyes
Goal: know who can approve what Claude proposes, and what to do when nobody can.
- Only owner and admin can approve, reject or revoke.
- The proposer cannot approve. For an agent's proposal, the person the agent acts for cannot approve either. A second owner or admin must.
- Executing is human-only and four-eyes too: not the proposer, and not the person whose agent proposed it. The approver, or another member, can execute.
- A proposal expires after 7 days. Propose again if it does.
- The console hides Approve only from the person who proposed. On an agent's proposal the creating person still sees the button, and the server refuses it with "An action proposed by your own agent must be approved by a different person". That is expected.
Approve something Claude proposed
- The approver opens Approvals (or Operate > Needs a human) and clicks the item. Needs a human
- Read what it will do. For a config change, the impact preview is shown.
- Click Approve, then Execute.
proposed to approved to executed. Ask Claude to read it back with gtm_approvals and gtm_lineage.You have no second approver
With one owner or admin, nothing can go live. Add a second one in Settings > Members, or from a terminal:
gtm member add --email approver@acme.example --password '<password>' --role admin --org <orgId>gtm member add is safe to re-run. The new member can approve in the console right away.Autonomy: how much Claude may do alone
The default is that every customer-facing effect needs approval. A person can widen this for one scope (the organization, a play or a lane) and can set budgets and time limits with envelopes. Claude can propose such a change. It can never grant it. The autonomy scale in the code runs from A0 Observe, through A1 Suggest and A2 Propose (the default), to A3 Act within policy and A4 Mission autonomy.
Stop an agent now
- One key: Settings > Developer, revoke it, or
gtm token revoke <key-id>. It stops working at once. - The whole agent, everywhere: Settings > Agents, Kill. Every key of that agent is refused at every checkpoint. Needs a human
- Bring it back: Settings > Agents, revive. Only a person can.
Prompts for this chapter
Tell me what my key can do
Call agent.whoami. List my effective scopes. Then tell me which of these I can do and which need a person: create a definition, approve a send, change Dry Mode, create a key, connect HubSpot. For each one that needs a person, say who and where in the console.- Claude does
- Reads
agent.whoamiand maps each task to its class and scope withgtm_describe. - You do
- Nothing, unless a scope is missing. Then create a new key with a wider preset.
Show what is waiting for approval and why
List every approval in state proposed with gtm_approvals. For each, name what it will do, who proposed it, and who is allowed to approve it, given that I created your key and cannot approve your proposals.- Claude does
- Reads the queue and each approval's attribution.
- You do
- Give the list to your second approver. Needs a human
Definitions as code
In GTM OS, your business logic is not hidden in settings screens. It is written down as definitions: small, versioned documents that the engines read at run time. You do not write them by hand. You tell Claude what you want, Claude writes the definition, and two different people make it live. This chapter shows every kind of definition, the change set that carries it, and the full life of one definition from your prompt to production.
What a definition is
Goal: know what you are asking Claude to write, and why nothing changes until a person signs it.
A definition has four parts:
| Part | Meaning |
|---|---|
kind | What sort of logic it is, for example definition.scoring_model or definition.lane. |
key | Its name inside that kind. Engines look up fixed keys, such as default and icp_fit for scoring. Keep those keys stable. |
spec | The settings. The schema is strict: an unknown field is rejected, never ignored. |
| Environment and version | Every save is a new version in one environment (dev, staging or prod). Old versions are kept. |
Three rules hold for every definition:
- Engines read only the signed version in the environment of the call. A draft or a proposed version is ignored. Where an engine ships a built-in default (scoring does), it runs that default until you sign your own.
- The proposer never approves or signs. If Claude proposes it, a different human approves it and a different human signs it.
- Production only accepts signed definitions that were promoted from a lower environment. You cannot write straight into
prod.
The config store inside your instance is the source of truth. A git repository is an optional mirror, not a requirement (gtm mirror export writes the store out to a repo).
config.list for each environment and config.promotion_status.Every definition kind
Goal: find the kind that holds the logic you want to change. 0.2.5 has 31 definition kinds, plus four other config kinds (ontology.field, ontology.object, ontology.relationship and package.manifest). Claude reads the exact field list from the bundled definition-kinds reference in your project, so you never need to memorize it.
| Area | Kinds | What they decide |
|---|---|---|
| Who to target and how they score | definition.scoring_model, definition.segment, definition.persona, definition.predicate, definition.committee_model, definition.customer_type | Fit and intent scores, which accounts a motion works, who the buyer is, the buying committee, your own customer types. |
| Lifecycle and stages | definition.journey, definition.stage_machine, definition.stage_map, definition.deal_taxonomy | The stages a company, person or deal moves through, the legal moves between states, what each CRM stage value means, your words for deal types. |
| Outreach rules | definition.lane, definition.sequence, definition.reply_labels, definition.brand_voice, definition.suppression_rule, definition.compliance_policy | Sending channels and caps, the steps of a cadence, how replies are labeled, tone and forbidden claims, who is never contacted, regulatory rules. |
| Plays, KPIs, money | definition.play, definition.data_monitor, definition.metric, definition.money, definition.unit_economics | Automations, data-quality checks, derived KPIs, where revenue truth lives, the economics ads pace against. |
| Data in | definition.identity_rule, definition.source_mapping, definition.binding, definition.enrichment_policy | How records match and merge, how an app's list maps into the ontology, how a capability is acquired and priced, how long enriched values stay fresh. |
| Platform | definition.agent, definition.skill, definition.app, definition.model_policy, definition.model_pin, definition.brand_kit | Agent authority and run bounds, skills, console screens, which model serves which task, theming. |
Some things you might expect to be a definition are not:
- Routing and territories. There is no routing definition kind. Territories are records, set with
territory.upsert, and member capacity is set withrouting.member_settings.set(see Routing). - Workflow automation. The qualified-account handoff and the signal reaction are feature flags (
qualification.handoff,signals.reaction), set withflags.set. - The autonomy level and Dry Mode. These are policy settings, not definitions (see Governance).
Only definition.data_monitor is sign-exempt: it is active the moment it is published. Every other kind needs a signature before an engine uses it in production.
A few kinds also have a dedicated "set" operation that publishes and signs in one step, as the person who calls it: brand.set, money.settings.set, deal_type.set, reply_labels.set and buying_roles.set. Agents cannot call them. Over MCP every call counts as an agent call, even when your own login is behind it. So you use the console screen (the Setup cards or the Settings pages), or Claude proposes the same change as a normal change set and a person signs it. stage_map.set is different: an agent may call it, and the call becomes an approval proposal. territory.upsert is human-only.
The change set: how a definition travels
Goal: understand the four calls Claude makes to carry a definition from an idea to a draft in dev.
A ChangeSet is a bundle of one or more definitions (it can mix kinds). Over MCP, each one is an envelope:
apiVersion: gtmos/v1
kind: definition.segment
metadata: { kind: definition.segment, key: mid-market-saas, organizationId: <orgId>, env: dev }
spec:
label: Mid-market SaaS accounts
objectType: gtm.account
criteria:
expression: "employeeCount >= 200 && employeeCount <= 2000"
variables: { employeeCount: int }Conditions use CEL, a small expression language. Every expression comes with a variables map that names the type of each field it reads (int, uint, double, bool, string, bytes, list or map). Claude writes both; validation catches a mismatch.
| Step | Operation (class) | What it does | Who |
|---|---|---|---|
| 1. Propose | config.propose_changeset (M) | Takes envelopes[] and returns a changeSetId in state draft. Nothing is applied. | Claude |
| 2. Validate | config.validate_changeset (Q) | Checks every envelope against its kind's schema and returns {ok, issues[]}. Issues are feedback, not errors. | Claude |
| 3. Preview | config.compute_impact (Q) | Writes an impact preview the approver can read before deciding. Optional. | Claude |
| 4. Apply | config.apply (M) | Does not apply anything itself. It proposes the publish as an approval. A different human approves and executes it, and only then does a version appear in dev. | Claude proposes, Needs a human approves |
Blueprint files and the git mirror use a flat form (kind, key, spec). The envelope form above is what the MCP operations take.
gtm_approvals shows the same to Claude. The proposal expires after 7 days if nobody decides.If it fails
VALIDATION_FAILEDwith an issue list: Claude fixes the spec and validates again. Typical causes are an unknown field, a missing required field, or a CELvariablesmap that does not match the expression.CONFLICTon apply, "stale": another version of one of these resources landed after the change set was proposed (for example a blueprint applied in between). Ask Claude to re-propose from the current state. The approver can also see anoverwrite: truerequest, which replaces the newer version on purpose.
The full life of one definition
Goal: follow one change, "qualify accounts at 65 instead of 60", from your prompt to production. Every definition follows these same steps.
| # | What happens | Operation | Who | State after |
|---|---|---|---|---|
| 1 | You ask. Claude reads the current definition and the environment it will write to. | config.get, config.list | You, Claude | unchanged |
| 2 | Claude writes the full new spec and runs the change set steps (propose, validate, preview). | config.propose_changeset, config.validate_changeset, config.compute_impact | Claude | ChangeSet draft |
| 3 | Claude proposes the publish. An approver reads the diff in Approvals, approves and executes it. | config.apply | Claude, Needs a human (not you if you created the agent key) | version vN in dev, status draft |
| 4 | Claude asks for sign-off on that version. | definition.propose_for_signing | Claude | status proposed |
| 5 | A human signs. Not the person who proposed it, and not the person whose agent proposed it. | definition.sign or definition.sign_many | Needs a human | status signed, still in dev |
| 6 | Claude proposes promotion of everything signed. One approval covers all of it, hop by hop (dev, then staging, then prod). | env.promote | Claude, Needs a human approves and executes | live in prod |
| 7 | Claude proves the engine now uses your version. | score.score_account, config.promotion_status | Claude | verified |
You need at least two humans with the owner or admin role. With only one, everything Claude proposes sits waiting. Add a second with Settings > Members.
Status values for a version: draft, proposed, signed, and deprecated at the end of its life (definition.deprecate, terminal, optionally with a sunset date and a notice window).
The same steps in the console
- Approvals and Operate > Needs a human: approve the change set, with its impact preview.
- CRM > Environments, Go live card: Review & sign all proposed (N) (step 5), then Promote dev to prod (step 6). Until it is done, Home, Apps and the engine pages show "N definitions are in dev, not live".
Where it goes wrong
| You see | Why | Fix |
|---|---|---|
| "My change is not used" | It is still in dev, unsigned, or you checked another environment. | Ask Claude for config.promotion_status. Sign, then promote. |
FORBIDDEN "An action proposed by your own agent must be approved by a different person" | You created the agent key, so you cannot approve what it proposes. | A second owner or admin approves. |
FORBIDDEN "... a different person must sign it (four-eyes)" | The signer proposed it, or is the human behind the proposing agent. | Another admin signs. |
| "Definitions cannot be published directly into a prod-class environment" | You tried to write to prod. | Publish to dev, sign, promote. |
| "Cannot promote ... the source version (vN) is draft, not signed" | Promotion needs a signed source. | Sign it first. |
| A promotion proposes nothing | Nothing signed differs from the target. | Check with env.promote and preview: true. |
The same path from a terminal
Goal: know the commands a person runs for the human steps, or when scripting. gtm plan, gtm apply and the others act in the environment you name with --env. Without it they use the instance default (prod), except that a blueprint of sign-gated definitions seeds dev. Always pass --env.
| Command | What it does |
|---|---|
gtm plan --env dev | Previews changes against live config. Add --from <blueprint file> to preview a blueprint. It takes a file path, not a catalog id. To apply a catalog blueprint by id, ask Claude to call blueprint.apply. |
gtm apply --env dev | Proposes a change set. Still needs a different human's approval. |
gtm definition propose-for-signing <kind>/<key> --env dev | draft to proposed. |
gtm definition sign <kind>/<key> --env dev | Human only. Signs one version. On a draft, it first proposes it, then signs; a different member must do the signing. |
gtm definition sign-all --env dev | Human only. Signs every proposed definition in the environment, except the ones you proposed. |
gtm promote --all dev prod --preview | Shows the plan: every resource, its version and diff, and what is skipped and why. |
gtm promote --all dev prod | Proposes one approval for the lot. |
gtm promote <kind> <key> dev prod | Promotes one resource. |
gtm rollback <kind> <key> --to-version 3 --env prod | Proposes a rollback (see below). |
gtm check | CI check. Plans across environments and exits non-zero on problems or drift. |
The sign commands need a person's own session (gtm login), not an agent key. An agent key is refused.
History, rollback and ownership
Goal: undo a bad change safely, and keep an audit trail of who owns each definition.
| You want | Ask Claude to use |
|---|---|
| Every version of one definition, newest first | config.history |
| A past version, or the state at a past moment | config.get with a version number or an as-of time |
| Roll back | config.rollback (kind, key, toVersion, env). It proposes. The rollback creates a new version that copies the old content. History is never rewritten. |
| See what drifted between environments | config.drift, or CRM > Environments |
| Retire a version, with a sunset date | definition.deprecate |
| Record who owns it, who signs it and why | definition.set_governance (owner, approver, rationale), read with definition.get_governance |
| Find definitions that say different things about the same concept | definition.detect_contradictions |
| See which definitions are healthy or at risk | definition.health |
config.get for that resource in the environment. It shows a new, higher version number whose content matches the old one.Prompts for this chapter
Goal: copy, adjust and paste. These prompts stop at proposals. Nothing here sends mail or spends money.
See what is live and what is waiting
Show me what is different between dev and prod. List every definition that is in dev but not live, which ones still need a signature, and who is allowed to sign each one.- Claude does
- Reads
config.promotion_status,config.listfor both environments, andconfig.drift. - You do
- Nothing. Read the answer.
Change one definition and walk me through the review
Raise the qualified threshold of the default scoring model from 60 to 65. Work in dev only. Show me the diff, validate it, and tell me exactly who has to approve, who has to sign, and what I click. Do not promote anything yet.- Claude does
- Reads
definition.scoring_modelkeydefault, proposes a change set with the full new spec, validates it and reports the impact preview, then proposesconfig.apply. - You do
- A different admin approves in Approvals. Then you continue with the next prompt.
Take it live
Everything I approved is now in dev. Propose it for signing, then preview the promotion to prod and show me the plan. Stop before you propose the promotion.- Claude does
- Calls
definition.propose_for_signingper version, thenenv.promotewithpreview: true. - You do
- A different human signs (CRM > Environments > Go live, Review & sign all proposed). Then tell Claude to propose the promotion, and a different human approves it.
Undo it
The new scoring threshold is too strict. Roll definition.scoring_model default back to the version before the last one, in prod. Show me both versions first.- Claude does
- Reads
config.history, shows both specs, proposesconfig.rollback. - You do
- A different human approves.
Prompts in this guide are untested drafts. Run each once on a dry-mode instance before you rely on it.
Build a system by prompting
You can build a full GTM system by talking to Claude. There are three entry points. /setup-gtmos installs and connects. The setup wizard turns what your company knows into definitions. /build-gtm-system builds engine by engine and checks each one live. This chapter explains what each one does, who has to click what, and how to start from a blueprint, the ten ready-made motions.
Which entry point to use
Goal: pick the right starting prompt.
| You are here | Use | It ends with |
|---|---|---|
| An empty folder, no instance yet | /setup-gtmos (the gtmos-setup skill) | A running instance, an agent key, connections checked, a readiness report. |
| An instance with a new organization, setup not finished | The setup wizard (console Setup, or "set up GTM OS for my company" with the gtmos-onboarding skill) | Approved definition cards, a build run, an OS brief, Launch. |
| An organization that exists and you want to build or change engines | /build-gtm-system | Each engine built, verified live, with decisions written down. |
| You want to know if it is safe to switch on | /go-live-check | One GO, NO-GO or N-A line per real check. |
All of them use the same operations as the console. What one saves, the others see.
/setup-gtmos: install and connect
Goal: go from an empty folder to a connected instance. The step-by-step version is in the Quickstart. This is what it means for you as an engineer.
- Claude runs each step itself and stops only where a person must act. It does not invent commands.
- It installs the instance into
./instance, writes the owner and approver passwords intoinstance/.env, and never prints them. - It mints its own agent key (preset Full GTM engineer), runs
gtm initandgtm doctor, and tells you to setGTMOS_AGENT_TOKENand restart. - It stores and tests connection keys you give it, links your Composio apps, and prints the SPF, DKIM and DMARC records for your sending domain.
Needs a human You sign in as owner and, in a second browser profile, as the approver. You click every OAuth consent. You add the DNS records.
/mcp shows gtmos as connected, and gtm doctor ends with a readiness summary.The setup wizard
Goal: turn answers, your website, connected data and your documents into the definitions the engines run on, with no invented values. Console: Setup. Over MCP: say "set up GTM OS for my company". The agent then follows the gtmos-onboarding skill, resumes at session.currentStep, and shows every draft with its evidence.
Three rules hold on every screen:
- No invented values. Every suggestion lists the facts it rests on. A value nobody knows stays empty and becomes a numbered open ask with an owner.
- Every saved answer changes engine behaviour. A card is published through the operation that owns that definition, with the same permissions, approvals and audit as anywhere else.
- The proposer never approves. Change sets, journeys, stage mappings, blueprints and promotions are approved by a different human. Definitions are signed by someone other than their proposer.
The six screens and the operations behind them
| Screen | You do | Operations Claude uses |
|---|---|---|
| 1. Welcome | Company profile, markets, motions, stack, pains, hours a week. Pick engines and a blueprint. | onboarding.update, onboarding.suggest, blueprint.get |
| 2. Connect your data | See real coverage, connect sources, pull now, send access asks. | onboarding.signal_coverage, onboarding.request_access, product_analytics.sync, transcripts.sync, outbound_rails.sync_replies |
| 3. Your definitions | Upload documents. Review each drafted card with its evidence. Edit, skip or approve. | onboarding.docs.upload, onboarding.draft_cards, onboarding.card.edit, onboarding.card.approve |
| 4. Rules and who approves what | Buying roles, consent, trust rules, autonomy per engine, seats and role profiles. | onboarding.card.approve, onboarding.seats.set, roles.list |
| 5. The build | Check engine readiness. Run the build. Clear what waits for a human. | onboarding.engine_readiness, onboarding.build.start, onboarding.build.get, onboarding.build.cancel |
| 6. First motion and 90 days | Shape the plan. Refresh the OS brief. Preview and run Launch. | onboarding.plan.set, onboarding.brief.refresh, onboarding.launch.preview, onboarding.launch |
Every write takes the expectedVersion from the last onboarding.get. A CONFLICT means someone else changed the session: read again, then redo the write.
What each card becomes
Approving a card publishes real definitions or settings. Here is where each one lands:
| Card | Published through | Who finishes it |
|---|---|---|
| Lifecycle stages | journey.propose and journey.apply | Pending until a different human approves. |
| ICP and the weights sheet | definition.scoring_model key icp_fit, by change set | Pending until a different human approves. |
| Pilot scope | definition.segment, by change set | Pending until a different human approves. |
| Customer types and their journeys | definition.customer_type, by change set | Pending until a different human approves. |
| Who the engines never contact | definition.suppression_rule, by change set | Pending until a different human approves. Do-not-contact values are stored as keyed hashes, never plain text. |
| Your CRM stage values | stage_map.set | A human's call is final. An agent's call is a proposal. |
| Revenue truth and currency | money.settings.set | Needs a human signed in. |
| Your words for deals | deal_type.set | Needs a human |
| Who owns what (territories) | territory.upsert | Needs a human |
| Brand voice and forbidden claims | brand.set | Needs a human |
| Buying roles | buying_roles.set | Needs a human |
| Consent and GDPR | privacy.settings.set | Needs a human |
| Dry mode and guardrails | policy.set_dry_mode, policy.guardrails.set | Needs a human |
| Autonomy per engine | policy.engine_modes.set, policy.set_autonomy_scope | Needs a human |
When an agent drafts the cards, it prepares each one and a human finishes the human-only ones in Setup. The agent never retries through another key. See Governance for what the autonomy card really does.
The build and Launch
- The build (
onboarding.build.start) queues a worker job that runs as you: it settles approved cards, adapts and proposes the chosen blueprint, suggests stage values, checks completeness, rescores, and writes the brief. Each step is logged with real results (onboarding.build.get). A failed step does not stop the others. A retry re-runs only failed or unfinished steps. - Adapting a blueprint. With
adaptToOnboarding,blueprint.applysubstitutes your approved card values into the blueprint first (ICP weights, pilot segment, journeys, customer types, suppression rules, and the daily cap of email lanes from the trust card).onboarding.blueprint_adaptationspreviews exactly what it will take. - Launch (
onboarding.launch, human only) signs and promotes every setup definition from the seed environment to the live one, and turns engines live only where the approved autonomy card says so.onboarding.launch.previewshows that without writing anything. Signing is four-eyes. Each promotion is approved by a teammate in Approvals.
onboarding.get shows the session and current step. onboarding.first_win lists each milestone from "a real data source is connected" to "the first live send" as done, pending or unknown, with the reason.If it fails
FORBIDDEN ... can only be performed by a human: expected. Claude prepares the draft and tells you which screen and button to use. Do not look for a workaround.CONFLICTwithLAST_OWNER_NARROWING: the change would leave nobody able to run the build, approve cards or launch. Make another member an un-narrowed owner or admin first. Never seat the only owner as Operator or Exec.- "Card is not valid yet": a required value is empty. Edit the card with your own value, or skip it.
- A draft says no model is configured: add a model connection in Integrations > Connections, then redraft.
- On an instance older than 0.2.5, these operations do not exist. Update the instance, then run
gtm init --refreshin each workspace.
/build-gtm-system: engine by engine
Goal: a full, customized setup from one conversation. Optionally give a goal: /build-gtm-system B2B SaaS sales-led, outbound plus SEO.
- Interview. Claude reads the live state first (
instance.readiness,blueprint.list,packages.list,config.list), then asks in batches: business and offer, ICP and tiers, personas, markets and compliance, channels, CRM and tools, data volumes, team and approver. It does not ask what the instance already knows. - Recommend. Claude picks the closest general blueprint and says why. You confirm.
- Seed. Claude applies it in
devas a proposal. - Engine by engine. Order: data first (CRM sync or import, Identity and data quality, Enrichment, Signals), then Score, then routing and plays, then Outreach, SEO and GEO, Ads. For each engine Claude explains it in two sentences, shows the defaults it would run today, asks only about the knobs that matter, writes the definitions, sends you to the console for any credential, and verifies live. Not green means not done.
- Go live. You approve the proposals, then sign and promote (see The full life of one definition).
Claude keeps two notes in your project. .claude/memory/decision-records.md holds each decision with its reason. .claude/memory/setup-status.md holds what a human still has to do.
Smaller commands for one job each:
| Command | What it does |
|---|---|
/define-object | Define or change an object type (fields, interfaces, stage machine). |
/create-play | Create and simulate a play. |
/wire-connector | Wire an external source or provider and set its binding mode. |
/build-app | Scaffold a record page, queue, table or dashboard. |
/onboard-blueprint | Seed a whole motion from a blueprint. |
/describe-ops | List operations, or show one operation's schema. |
/compile-rule | Turn one rule from the GTM playbook into a signed definition or an engine operation, with tests. |
/design-abm, /set-brand-split, /tune-economics | Design an ABM motion, set the brand and activation split, tune with real unit economics. |
/go-live-check | GO or NO-GO from real checks only (gtm doctor --strict, instance.readiness, config.promotion_status and more). Anything it cannot check reads NO-GO "unverified", never GO. |
Blueprints and motions
Goal: start from the closest ready-made motion, then customize. A blueprint seeds a whole motion (personas, segments, scoring, suppression, bindings, lanes, plays, metrics) in one change set. They are general templates. Do not ask Claude to create one per customer. Apply the closest, then customize it through the normal path (Chapter 5).
| Id | Fits | Distinctive pieces (definitions seeded) |
|---|---|---|
b2b-saas-sales-led | B2B SaaS with a sales team | Account identity rule, opportunity stage machine, compliance policy, two personas, lane sales-outbound (34) |
enterprise-abm | Named accounts, buying committees | Account-journey stage machine, three personas, committee model, lane abm-multithread, play account-play (34) |
b2b-saas-plg | Product-led, product-qualified leads | Segment plg-active-workspaces, lane plg-nurture, metric pql-accounts (17) |
devtools | Developer tools, developer relations | Personas developer and eng-leader, lane devrel (16) |
agency-services | Agency selling services to small businesses | Outside-in scoring, lane services-outreach, bounce-rate monitor (21) |
b2b-marketplace | Two-sided marketplace | Supply and demand personas and segments, lane supply-acquisition (18) |
dtc-b2c | Consumer brand, repeat purchase | Lane lifecycle-email, definition.unit_economics default, play winback, ad pacing (14) |
outbound | Just outbound email to start | Persona, segment, scoring, lane primary-email, a signal_follow_up sequence and signal-to-sequence plays (19) |
seo | Just SEO and GEO | Priority-pages segment, freshness and volume monitors, daily-seo-audit. Requires Google Search Console (15) |
full-gtm | Everything, including SEO and ads | The sales-led foundation plus scoring, committee, enrichment, the email lane, CRM sync, SEO and GEO pieces. Requires Google Search Console (49) |
Every blueprint also lists the connections it needs (a model provider, an email sender, looot, a CRM, an ad platform). Applying one makes those connections required in readiness. Claude reads the exact list with blueprint.get. Only seo and full-gtm require Search Console.
Apply one
- Claude lists and reads:
blueprint.list, thenblueprint.getwith the id. - Claude applies it with
blueprint.apply:id,env(defaultdev),senderPhysicalAddress(stamped on every email lane that has none),parameters,gscProperty(required for a search blueprint on a live instance), and optionallyadaptToOnboarding. It first runs a validator. It drafts a change set of only the resources that differ, then proposesconfig.apply. Applying the same input again while that proposal waits returns the same proposal. - Needs a human A different admin approves. The definitions land in
dev, unsigned. They go live after sign and promote.
Other entry points: gtm init --org <id> --blueprint <file>, gtm onboard, and gtm setup ... --blueprint <id>. To pick up a newer version of a blueprint you already applied, gtm blueprint upgrade shows a diff first and applies through the approval gate only with --yes.
config.list with environment: "dev" shows what was seeded.If it fails
VALIDATION_FAILEDwith a list: the validator found an unknown operation, a placeholder value, an email lane with no sender postal address, or a search blueprint with no Search Console property. Fix the input it names.- The plays of a search blueprint are held back (
blocked[]) until Search Console is live.blueprint.requirement_statussays what is missing.
Customize after applying
- Read what was seeded (
config.list,config.get). - Change only what your interview requires: ICP criteria, weights, thresholds, lane caps, brand voice, play steps. Keep keys stable (
defaultscoring model, lane keys) so plays and engines still resolve them. - Propose, approve, sign, promote (Chapter 5). Chapter 7 gives the prompt for each kind of logic.
- Record the blueprint and every deviation in
decision-records.md.
Prompts for this chapter
Goal: the first prompts to run, in order. None of them sends mail or spends money.
Run the setup wizard
Set up GTM OS for my company. Start by reading where setup stands. Use my website to draft what you can, show me the evidence for every value, and only save what I confirm. Turn every gap into a numbered open ask.- Claude does
- Reads
onboarding.get, resumes at the current step, drafts cards with evidence, and saves only confirmed answers. - You do
- Answer, upload documents, and finish the human-only cards in Setup. A different admin approves the change sets.
Apply the closest blueprint and customize it
We sell to about 200 named accounts with five-person buying committees. Recommend the closest blueprint and explain why. Preview what it seeds with its connections, then apply it in dev as a proposal. Then list what you would change for us. Do not promote anything.- Claude does
- Compares blueprints with
blueprint.listandblueprint.get, recommends one (here likelyenterprise-abm), and callsblueprint.apply. - You do
- A different admin approves the proposal in Approvals.
Build the system
/build-gtm-system B2B SaaS, sales-led, selling to mid-market in the DACH region. Work in dev. Go engine by engine, data first. For each engine show me today's defaults before you change anything, and stop at every proposal that needs a person.- Claude does
- Runs the interview, then the engine loop, and keeps
decision-records.mdandsetup-status.mdup to date. - You do
- Answer the interview, connect credentials in the console, approve, sign, promote.
Where am I?
What is my next step to get live? Check engine readiness, my first-win progress, and everything waiting on a person. Tell me who has to do each thing.- Claude does
- Reads
onboarding.engine_readiness,onboarding.first_win,config.promotion_statusandruntime.list_approvals. - You do
- Nothing. Read the list.
Prompts in this guide are untested drafts. Run each once on a dry-mode instance before you rely on it.
Customize your business logic
This is the chapter you will come back to most. Each section covers one kind of GTM logic: what you can change, which definition or operation to ask Claude to use, a prompt you can paste, how to verify it worked, and what to watch out for. Every change follows the path in Chapter 5: Claude writes it in dev, a different human approves, a different human signs, and a promotion makes it live. This chapter does not repeat that path. It says which parts are human-only.
Where each kind of logic lives
Goal: find the right section fast.
| Your question | It lives in | Section |
|---|---|---|
| Who is a fit, and what is a good signal? | definition.scoring_model, definition.segment, definition.persona | ICP and scoring |
| When is an account qualified, and what happens next? | definition.committee_model, flag qualification.handoff | Qualification |
| Who owns it? | Territories and member capacity (records, not definitions) | Routing |
| What do we do, in what order, and how fast? | definition.play, definition.sequence, definition.lane | Plays, sequences, cadences |
| Who must we never contact, and what does the law require? | definition.suppression_rule, definition.compliance_policy, guardrails, consent | Suppression and compliance |
| What may we say, and in what voice? | definition.brand_voice, the proof library, the claims policy | Claims, voice, proof |
| When are two records the same, and who wins? | definition.identity_rule | Identity and merge rules |
| How do we measure it? | definition.metric, definition.money, definition.data_monitor | Metrics and money |
| What stages does a record move through? | definition.journey, definition.stage_machine, definition.stage_map | Stage machines and journeys |
ICP and scoring models
Goal: the Score engine ranks accounts the way you would. Three models work together: icp_fit (firmographics), default (intent signals) and signals (signal strength).
| What you can change | Where |
|---|---|
| Fit rules: country, size, industry, anything on the account, as a CEL condition with a weight | definition.scoring_model key icp_fit, rules[] with matchesKind: [firmographic] and a condition |
| Signal weights per kind (funding, hiring, expansion, pricing-page visit...) | key default, rules[] (matchesKind, weight) |
| The line for "qualified" and "needs review" | qualifiedThreshold, needsReviewThreshold |
| How fast signals fade | composite.intentHalfLifeDays, and per family in composite.intentHalfLifeByFamily (buying, intent, engagement, product, lifecycle, negative) |
| How much first-party evidence outweighs third-party | composite.partyTierMultipliers (first, second, third, unknown) |
| The mix of fit and intent, and the tier names | composite.fitWeight, intentWeight, verdictBasis, tiers[] |
| Hard caps (competitors, wrong region, anything) | disqualifiers[]: cap and a when block (relationship status, a field value, signal kinds in a window, or a suppression reason) |
| Penalties for negative replies and hard bounces | negatives: replyLabels[] with a penalty, and hardBounce |
| Different weights for different segments, with bands that decay without a rep touch | segmented block |
| Who a "persona" or "segment" is | definition.persona, definition.segment (CEL criteria) |
Ask Claude to use: definition.scoring_model (keys default, icp_fit, signals), definition.segment, definition.persona. For example, an ICP fit model for the DACH region looks like this when Claude proposes it:
kind: definition.scoring_model
key: icp_fit
spec:
scoreScaleId: icp.fit.0-100
qualifiedThreshold: 70
needsReviewThreshold: 30
rules:
- id: region
statement: Headquartered in DACH
matchesKind: [firmographic]
weight: 40
condition: { expression: 'countryCode in ["DE", "AT", "CH"]', variables: { countryCode: string } }
- id: size
statement: 200 to 5,000 employees
matchesKind: [firmographic]
weight: 40
condition: { expression: "employeeCount >= 200 && employeeCount <= 5000", variables: { employeeCount: int } }Replace the default ICP with ours
Our ICP: B2B software companies in Germany, Austria and Switzerland with 200 to 5,000 employees. Hiring for sales roles is a strong sign. Competitors must never score. Read our current icp_fit and default scoring models in dev, show me the changes side by side, run a rescan preview, and propose the change set. Work in dev. Do not rescan the organization.- Claude does
- Reads
config.getfor both keys, drafts the new specs, validates them, runsscore.rescan_preview(no writes) to show who would move into or out of qualified, then proposesconfig.apply. - You do
- A different admin approves and, later, signs and promotes (Chapter 5).
score.score_account on a real account reports the policy version definition.scoring_model:default:vN (yours), not score.default.v2. score.explain_subject shows why: factors, decay, caps, negatives. Console: Engines > Score, and the Why this score panel on a company page.Watch out for
- The built-in
icp_fitis neutral. With nothing signed, it scores company size only. It has no region, industry or team preference. Always sign your own. - Wrong environment. Sign in dev and score in prod, and the engine silently uses the default.
- Rules need real signal kinds. A rule that matches a kind nothing produces never fires. The blueprint validator flags this.
- Rescan after a change.
score.rescan_organizationre-scores accounts whose scores are stale or were made under an older model. Runscore.rescan_previewfirst. - Learning.
score.learnproposes a new model version from real outcomes (replies, meetings, opportunities, wins), moves rule and segment weights only, and never signs. Caps, negatives, multipliers and half-lives never change by learning.score.feedbackrecords a rep's "too high, too low, wrong reason". Turning the weekly auto-proposal on or off (score.learning_settings.set) is human-only. - Engine detail is in Score, Signals and Enrichment.
Qualification and buying committees
Goal: decide what "qualified" means for you, who must be on the buying committee, and what happens the moment an account qualifies.
| What you can change | Where |
|---|---|
| The score at which an account is qualified or needs review | qualifiedThreshold and needsReviewThreshold in the scoring model (see above) |
| The roles a healthy deal must cover, and the champion role | definition.committee_model key default: requiredRoles, championRole, recencyWindowDays, and your own roles[] |
| Your vocabulary of buying roles | buying_roles.set (human-only: publishes and signs the committee model), or the committee model by change set |
| What happens when an account qualifies | Feature flag qualification.handoff, set with flags.set (also Settings > Workflow automation) |
| How the engines react to a strong new signal | Feature flag signals.reaction (on by default; Settings > Workflow automation). Its value can set enabled, rescore, and for route, enrol and alert either false or an object with minStrength (defaults 0, 50 and 60) and, for enrol and alert, categories |
The qualification.handoff flag is on by default. Its value is strict: any key not listed here is refused.
| Key | Meaning |
|---|---|
enabled | Turn the whole handoff off with false. |
route | false to skip owner assignment, or { ownerIds, territoryKey } to narrow the pool. An unowned account is routed round robin through routing.assign, so capacity and out-of-office apply. |
task | { enabled, dueInDays }. Default: a follow-up task due in 2 days. |
enrol | false to skip enrolment, or { sequenceId, laneKey, maxContacts }. maxContacts runs 1 to 10 (default 1). |
disqualifiers | relationshipStatuses (default customer, partner, former_customer), negativeKinds (default risk, churn_risk), negativeWindowDays (default 30), openDealBlocksEnrol (default true). |
Change what happens on qualification
When an account qualifies, route it only among these people: <names or ids>. Make the follow-up task due in 1 day and enrol at most 2 contacts. Keep every other setting of the qualification.handoff flag as it is. Read the flag first and show me the old and new values before you save. Do not enrol or send anything.- Claude does
- Reads
flags.get, merges your change into the whole value, and writes it withflags.set. Enrolment and sends still go through the approval gate. - You do
- Nothing for the flag. The later sends need approval as usual.
Define the buying committee
Our deals need an economic buyer, a champion, a technical evaluator and a legal contact. The champion must be mandatory. A role counts as covered if someone in it was touched in the last 45 days. Propose the committee model in dev.- Claude does
- Proposes
definition.committee_modelkeydefaultwithrequiredRoles,championRoleandrecencyWindowDays: 45. - You do
- Approve, sign, promote.
score.committee_coverage for one account lists which required roles are identified, engaged and recently touched. score.rescan_preview lists the handoffs a rescan would fire, writing nothing. flags.get returns the saved value.Watch out for
flags.setoverwrites the whole value. If Claude writes only the key you named, you lose the others. Say "keep the other keys".- An invalid value stops the handoff with
VALIDATION_FAILEDnaming the flag. - Customers, partners and former customers are not handed off by default. An open deal blocks enrolment. Remove these defaults only on purpose.
- Enrolment skips anyone suppressed or with no email, and says why.
Routing, territories and round robin
Goal: every account, contact and deal gets the right owner, fairly, with a record of why.
There is no routing definition kind. Territories are records and capacity is a per-member setting. Both are saved by a person (human-only: agent keys are refused). Claude can preview any change before you save it, and can run assignments itself.
| What you can change | Operation |
|---|---|
| A territory: name, kind (geographic, segment, named_account, vertical), match rules, owner or rotation, priority, active | territory.upsert (human-only). Match: countries, regions, industries, segments (or the bands smb under 200, mid_market 200 to 999, enterprise 1000 and up), customerTypes, employeeMin, employeeMax, accountIds, domains. Groups are ANDed, values inside a group are ORed. An empty match never routes. |
Territory policy: keep a record with its account's owner (sticky), exempt that owner from caps, attach subdomain leads, and what happens when nobody is eligible (overflow: a named owner, or a queue label) | territory.upsert policy |
| Per-person capacity: open cap and weekly cap per record type, weight 1 to 100, active, out-of-office window | routing.member_settings.set (human-only). Read with routing.member_settings.list. |
| Assign one record, by name or round robin (optionally from a pool you give) | routing.assign (strategy round_robin or territory, ownerPool, keepExistingOwner, skipCustomers) |
| Give every unowned account an owner | routing.reconcile |
| See which territory a record would get, and why | territory.match (read-only) |
How a winner is chosen: named-account territories always win. Then the lowest priority number, then the most matching criteria, then the name. Round robin skips anyone who is inactive, out of office or at their cap.
Design the territories
Design territories for us. DACH accounts with 1,000 or more employees go to the enterprise team (rotate between <two names>). Everything else in DACH goes to the mid-market team. Our top 20 named accounts (I will paste the domains) always go to their account owner. Show me every territory with its match rules, priority and rotation, then preview three real accounts with territory.match. Do not save anything. Tell me the exact values I need to enter in Engines > Routing.- Claude does
- Reads members with
routing.member_settings.list, drafts the territory values, and previews real accounts withterritory.match. - You do
- Needs a human Save the territories in Engines > Routing (or sign in and call
territory.upsert).
territory.match returns the winner, every territory's criterion-by-criterion result, and an explanation. After an assignment, each subject has an append-only routing Decision Record. Console: Engines > Routing (territories, manual assignment, history) and the owner column on the Hot list.Watch out for
- Territories and capacity are records and settings, not definitions. They are not signed or promoted. They apply when a person saves them.
- Inactive territories never route. An out-of-office member is skipped even for sticky ownership.
routing.assignwith a named owner skips round robin. An inbound play should passkeepExistingOwnerso a rep's lead is never taken, andskipCustomersso customers are left alone.
Plays, sequences and cadences
Goal: automate the motion: what triggers, what it does, in what order, how fast, and how it stops.
Plays
| What you can change | Where in definition.play |
|---|---|
| When it starts | trigger.kind: manual; event (eventType, optional subjectFilter); schedule (everyMinutes or dailyAt with a timezone, optionally an audience); metric (a monitor); webhook (HMAC signed); segment (entry, exit or both); mission_step |
| Who it applies to | filter (CEL), customerTypes |
| What it does | steps[]: action (an operation and its input), wait (a duration in seconds, until a field, or a named signal), branch (cases with conditions, plus an else) |
| Extra precondition | gate on the play, or on one action. It never replaces human approval. |
| Limits | guardrails: maxActionsPerRun, maxBudgetUsd, rateLimit (maxPerWindow in windowMinutes) |
| Reporting | outcomes[] |
Scheduled plays can aim at a built-in audience: deal.stage_stuck, deal.closed_lost, subscription.renewal_due, contact.champion_moved, meeting.ended, content.refresh_due. A play can start on any of 15 events, such as gtm.signal.observed, gtm.record.created, gtm.meeting.booked, gtm.reply.received and gtm.transcript.ingested. The full list with filter fields is in the reference. Event plays skip events from bulk writers (imports, CRM sync) unless the play sets includeBulkSources. Step inputs can read the trigger and earlier steps with {{ trigger.subject.id }} and {{ steps.<id>.output.<path> }}.
Sequences and cadences
| What you can change | Where in definition.sequence |
|---|---|
| The steps (1 to 20), each on email or LinkedIn | steps[]: key, channel, wait (0 to 90 days or business days) |
| The words | A fixed template (subject, body with merge fields such as {{firstName|there}}), or a brief for the drafting agent, which grounds claims in approved proof |
| When it may send | sendWindow: start, end and ISO weekdays, in the recipient's local time |
| A/B tests | variants[] (2 to 5, each with a weight) |
| Depth of personalization | personalisation: auto, 1:1, 1:few, 1:many |
| LinkedIn actions | linkedin.action: message, connect, connect_then_message (with an accept wait and a timeout rule), inmail |
| When a person leaves it | exits: reply, unsubscribe, bounce and manual stop always; meetingBooked and stageChange by choice |
A sequence with no signed definition.sequence keeps the classic cadence: one step every 3 days. Bind a signed step plan with sequence.update. Cadence timing runs through outreach.rescan_cadences (drafts the next step for anyone due) and outreach.advance_cadence (forces one step for one person).
Lanes: the sending rules
definition.lane sets a channel's limits: dailyCap, recentTouchWindowHours, openDealPolicy (block or allow), enforcedChecks, envelopeBudgetEur over windowDays, bounceRatePauseThreshold, complaintRatePauseThreshold, sendingDomain, senderPhysicalAddress, and for LinkedIn the seat and its daily and weekly caps. Every send needs an active lane. lane.activate proposes activation. A different human approves it. In a production environment it is refused unless SPF, DKIM and DMARC pass for the sending domain. The console has no activate button.
Build a play and test it before it runs
Create a play called stuck-deal-nudge. Every weekday at 08:00 Europe/Berlin it looks at deals that have sat in a stage for more than twice that stage's median time, at most 20 per run, and creates a follow-up task for the deal owner. No emails. Propose it in dev, then simulate it over the last 30 days and show me what it would have done. Do not trigger it.- Claude does
- Proposes a
definition.playwith ascheduletrigger and adeal.stage_stuckaudience, oneactionstep, andmaxActionsPerRun. Runsplay.simulate(zero writes). - You do
- Approve, sign, promote. Pausing or resuming a live play's own trigger (
play.binding.set_status) is human-only.
Write a sequence with an A/B test
Write a 4-step email sequence called signal_follow_up_v2. Step 1 at once, then +3, +5 and +7 business days. Send only 08:30 to 11:30 recipient time, Monday to Thursday. Test two subject lines on step 1, 50/50. Steps use a brief, and claims must come from approved proof only. Exit on reply, unsubscribe, bounce, or a booked meeting. Propose it in dev. Do not create, enrol or send anything.- Claude does
- Proposes a
definition.sequencewithsendWindow, twovariantsof weight 50, andexits. - You do
- Approve, sign, promote. Later, a different human approves each real send.
Set the sending rules for a lane
Propose a lane called primary-email: email, at most 80 sends a day, no contact touched in the last 7 days, block anyone with an open deal, enforce suppression, consent, daily cap, recent touch and open deal checks. Pause it at a 4 percent bounce rate or 0.1 percent complaint rate. Sending domain mail.example.com. Do not activate it.- Claude does
- Proposes a
definition.lanein dev. You provide the sender postal address if it asks. - You do
- Approve, sign, promote, then ask Claude to run
lane.activateand have a different human approve it.
sequence.members shows who is in and why. outreach.step_stats shows sends, replies, positive replies, meetings and bounces per step and A/B variant.Watch out for
- Every send is an effect. It is proposed, and a different human approves it. A first message to a net-new contact always waits for a human, at every autonomy level. In Dry Mode it stays a draft. See Governance.
- "Lane required" or "mailbox required": the lane is not signed, promoted or activated, or no sender is connected.
- Play steps must name real operations. The blueprint validator rejects unknown ones.
- A play only runs from a signed definition in the environment it fires in.
- Always simulate first.
gtm play simulate <playKey> --env dev --since 30ddoes the same from a terminal.
Suppression and compliance
Goal: the system never contacts who it must not, and follows the rules of each region.
| What you can change | Where |
|---|---|
| Who is never contacted: competitors, customers, opted out, a do-not-contact list | definition.suppression_rule: label, reason, CEL criteria. A rule's reason also feeds scoring caps. |
| Rules a message or action must satisfy (consent, region, claims) | definition.compliance_policy: jurisdiction and rules[], each with a prohibitedIf condition |
| The standard regional policies (GDPR for the EU, DE and FR, PECR for the UK, CAN-SPAM, CASL) | compliance.seed_policies, once per environment |
| GDPR mode: a lawful basis per channel is needed before outreach or enrichment | privacy.settings.set (human-only) |
| A contact's consent per channel (email, phone, sms, whatsapp, linkedin) | consent.set, consent.withdraw; read with consent.get and consent.history |
Org-wide limits: emails a day, first-touch accounts a week, daily ad spend, a send window in the recipient's time, allowed channels, custom caps with defer or require_approval | policy.guardrails.set (human-only; replaces the whole spec). Read with policy.guardrails.get. |
| Pre-send checks per lane | definition.lane enforcedChecks: suppression, consent, mailbox_health, daily_cap, template_integrity, declared_params, recent_touch, open_deal, reply_stop |
| What a reply does (stop a sequence, suppress, open a task) | definition.reply_labels (effects per label); reply_labels.set is human-only |
A suppression rule from a shipped blueprint looks like this:
kind: definition.suppression_rule
key: opted-out
spec:
label: Opted-out contacts
reason: Contact requested no further outreach
criteria:
expression: 'has(consent.status) && consent.status in ["withdrawn", "denied", "expired", "opted_out"]'
variables:
consent: mapAdd suppression and compliance for a region
We start prospecting in Germany and France. Seed the standard compliance policies into dev. Add a suppression rule for our competitors and our current customers (I will paste the domains). Then show me how our email lane would treat a German contact with no recorded consent. Propose changes only, send nothing.- Claude does
- Runs
compliance.seed_policiesfor dev, proposesdefinition.suppression_rulerules by change set, and readsconsent.getand the lane'senforcedChecksto explain the outcome. - You do
- Approve, sign, promote. Switch GDPR mode on in the console if you need it. Set the guardrails in the console too.
consent.get says whether a send on a channel is allowed right now. policy.guardrails.usage shows today's and this week's counts against each cap. policy.trust_holds.list lists actions a guardrail is holding (for example, quiet hours). sequence.enroll into a draft sequence lists who was skipped and why (suppressed, not contactable, no email).Watch out for
- An email lane needs a sender postal address on a live instance. The validator refuses the blueprint without one.
- The setup wizard stores a do-not-contact list as keyed hashes, never as plain emails or domains. Only an owner or admin sees the source list.
- Guardrails and GDPR mode are in effect as soon as a person saves them. They are not signed or promoted.
- A suppression rule is only active once signed in the environment where sends happen.
Claims, voice and proof
Goal: every message and page sounds like you and claims only what you can prove.
| What you can change | Where |
|---|---|
| Tone, preferred words, avoided words, example phrases, voice samples (up to 20) | definition.brand_voice key default: tone[], vocabulary, examplePhrases, voiceSamples[] |
| Claims you must never make | forbiddenClaims[] (up to 200): phrase, reason, match as phrase or word |
| The proof a claim can rest on: case studies, metrics, testimonials, logos | The proof library: proof.create, proof.extract_from_document (reads a PDF, Word, Excel, CSV or text and proposes claims, each quoted word for word), proof.update, proof.archive; read with proof.list, proof.get, proof.usage |
| Making proof count | proof.propose_activation (human-only; a different person approves). Each proof has a permission and an expiry. |
| What happens to a claim with no approved proof | policy.claims.set (human-only): mode block (default, the proposal is refused) or route_to_approver (the approver acknowledges each flagged claim with runtime.acknowledge_claim before approving), and needsReviewThreshold from 0.2 to 1 (default 0.5) |
Brand voice by change set works for Claude. brand.set publishes and signs in one step, but only a person can call it. Where to click: Settings > Brand & compliance (voice and forbidden claims), Settings > Proof library (proof, and the Claims check tab for the policy).
Set the voice, forbid claims, load proof
Our voice: plain, direct, no hype. Avoid: "revolutionary", "best-in-class", "synergy". Never claim: "guaranteed results" or "number one". I am attaching a customer case study PDF. Extract draft proof and claims from it and show me each claim next to the sentence it came from. Propose the brand voice in dev. Do not activate any proof, and do not draft any message.- Claude does
- Proposes
definition.brand_voicewithvocabulary.avoidandforbiddenClaims. Runsproof.extract_from_documentto create a draft proof source, and lists its claims. - You do
- Review the claims. Needs a human proposes activation and a different person approves it. Approve, sign and promote the brand voice.
brand.check_text scans any text against the forbidden claims and avoided words, the same scan every draft goes through. proof.usage shows where each claim backed copy. policy.claims.get shows the current mode.Watch out for
- A word cannot be both preferred and avoided or forbidden. The change is refused.
- Claims are checked again when an approved action executes. A proof revoked or expired after approval blocks the send.
- In
route_to_approvermode, no policy or envelope may auto-approve the action. - Extracted claims are drafts. Nothing backs copy until a different person approves the activation.
Identity resolution and merge rules
Goal: decide when two records are the same company or person, which source wins each field, and what a merge keeps.
There are two layers. Both read the same signed definition.identity_rule per object type (gtm.account, gtm.contact).
| Layer | What it does | Operations |
|---|---|---|
| Observation to entity | Decides which entity a new piece of data belongs to. Defaults: an account matches on domain (authoritative) and name (corroborating). A contact matches on primaryEmail and displayName. Matching runs deterministic, then blocking, then probabilistic. Unsafe matches go to a review queue. | resolve.review.list, resolve.review.decide (link, new_entity or reject), resolve.merge, resolve.split |
| Record dedupe (Identity and data quality) | Finds duplicate pairs and merges them under approval, with a field-by-field preview. | identity_dq.scan_duplicates, identity_dq.preview_merge, identity_dq.merge_records, identity_dq.revise_merge, identity_dq.unmerge_records |
What you can change in definition.identity_rule | Field |
|---|---|
| Which keys match, and how strongly | rules[]: key and role (authoritative, corroborating, forbidden) |
| Who wins a field when sources disagree | survivorship.default and survivorship.perProperty: most_recent, highest_confidence, confidence_weighted, first_write_wins, longest_non_empty, or source_priority with an order list |
| What a merge keeps | survivorship.mergePerProperty and mergeDefault, which also allow keep_survivor and manual |
| Match tuning | matching: weights, minConfidence (0 to 0.99), nameSimilarity (0.5 to 1) |
The easy way to change merge rules: identity_dq.merge_rules.get reads the effective rules, and identity_dq.merge_rules.propose drafts a new definition.identity_rule version as a change set in dev. Nothing changes until it is approved, signed and promoted.
Change which source wins
When two company records disagree on the company name, the CRM must win over any other source. For employee count, take the highest-confidence value. Show me the effective merge rules today, then propose the change in dev. Then scan accounts for duplicates and preview three merges with identity_dq.preview_merge. Do not merge anything.- Claude does
- Reads
identity_dq.merge_rules.get, proposes withidentity_dq.merge_rules.propose, runsidentity_dq.scan_duplicatesandidentity_dq.preview_merge(both read-only). - You do
- Approve, sign, promote. Each real merge is a separate proposal.
identity_dq.quality_report before and after shows fill rate, duplicate rate, staleness and mean quality. identity_dq.preview_merge lists fields filled and fields in conflict and how each is decided. Console: Engines > Merges, CRM > Data quality, and Operate > Needs a human for review items.Watch out for
- A merge is an effect:
identity_dq.merge_recordsis proposed and a different human approves. It is reversible withidentity_dq.unmerge_records, which keeps fields edited after the merge. - A person can pick surviving values with
identity_dq.revise_merge. It re-proposes the merge, and a third person approves. - Automated writes onto existing records must resolve to the existing record (by domain, email or source id). A write that cannot resolve must report it, not create a duplicate.
- Run the data-quality loop after every import: report, normalize, backfill domains, scan, merge, report again.
Metrics, KPIs and money
Goal: the numbers on your dashboards mean what your team means, and revenue is counted the way finance counts it.
| What you can change | Where |
|---|---|
| A derived KPI over any object | definition.metric: objectType, aggregation (count, sum, avg, min, max, ratio), field, a CEL filter, and for a ratio a numerator |
| A KPI that is a slice of another (variants must never sum to more than the whole) | parentKey. Check with metric.verify_rollup. |
| A data-quality check with a threshold and schedule | definition.data_monitor (active when published; no signature needed) |
| Where revenue truth lives (CRM deal amounts, or a warehouse or billing object you have mapped in) | definition.money revenueTruth |
| How revenue is credited to engines, campaigns, channels and plays | attribution: windowDays (1 to 365), model (equal_split, position_based, first_touch, last_touch, time_decay) |
| The exchange-rate rule | fx.convention: market_daily or finance_dated (finance's own rate sheet, money.rate_sheet.set, human-only) |
| The reporting currency | crm.settings.set |
money.settings.set publishes and signs definition.money in one step, and only a person can call it. Claude can propose the same change by change set. Settings page: Settings > Money.
Define a KPI
Define a metric called qualified-dach-accounts: the count of accounts in DACH that are qualified. Define a second, qualified-dach-enterprise, as a slice of the first for companies with 1,000 or more employees. Propose both in dev, then check that the slice rolls up to the whole.- Claude does
- Proposes two
definition.metricversions (the second withparentKey), then runsmetric.evaluateandmetric.verify_rollup. - You do
- Approve, sign, promote.
Check the money
Show me where our revenue truth comes from today, the attribution model and window, and the FX convention. Then reconcile revenue truth against the CRM's won-deal amounts for the last 6 months and tell me where they differ. Change nothing.- Claude does
- Reads
money.settings.get, thenmoney.reconcileandmoney.fx_drift. - You do
- Decide, and use Settings > Money if you want to change it.
metric.evaluate returns the number (it compiles the definition to a parameterized SQL aggregate). Compare it to a count you trust. Dashboard tiles in Mission Control and the engine pages use the same definition. money.reconcile shows drift per month and per account.Watch out for
- Two metrics with the same name but different logic confuse everyone.
definition.detect_contradictionsfinds them. - Changing revenue truth rewrites no stored rows. Every report applies the new setting on its next read, so the numbers change at once, after signing.
- A records-based revenue truth must name an existing custom object and its fields.
Stage machines and journeys
Goal: your lifecycle words and stage rules replace the generic ones, and CRM stage values map onto them.
| What you can change | Where |
|---|---|
| A lifecycle for companies, people or deals: ordered stages, each open, won or lost | definition.journey: objectType, stages[] (key, label, category), isDefault, priority, segments routed to it, optional pipelineId and stageMachineKey |
| The legal moves between states, with guards | definition.stage_machine: states, initialState, transitions[] (from, to, optional CEL guard). Transitions become transition operations such as deal.advance_stage. |
| What each value in a CRM's stage field means | definition.stage_map (key <system>.<account|contact|deal>): map to a journey stage, or ignore. An unmapped value is held for review, never defaulted. Use stage_map.unmapped, stage_map.suggest, stage_map.ai_suggest, stage_map.set. |
| Your kinds of customer | definition.customer_type: CEL criteria, priority (a company takes the first type it matches), up to 3 journeys |
| Your words for deal types | deal_type.set (human-only), or definition.deal_taxonomy by change set |
| Deal pipelines and their stages | pipeline.create, pipeline.stage.create, pipeline.stage.update, pipeline.stage.reorder (Settings > Pipelines) |
Propose a journey with journey.propose. It builds and validates the change set (unique stage keys, at least one open stage, valid links, one default journey per object type). journey.apply submits it for approval. After that comes sign and promote.
Define the lifecycle and map the CRM
Our deal lifecycle is: discovery, evaluation, proposal, negotiation, won, lost. Propose it as a journey for deals in dev, with a stage machine that does not allow skipping from discovery to won. Then list every stage value in our CRM that is not mapped yet, suggest a mapping for each with the reason, and do not save any of them.- Claude does
- Calls
journey.propose(with a linkeddefinition.stage_machine), thenstage_map.unmappedandstage_map.suggest. - You do
- A different human approves
journey.apply. You decide each stage mapping, in Settings > Journeys or by telling Claude to callstage_map.set(an agent's call becomes a proposal).
journey.funnel shows records per stage, days in stage, deal value and the share at or beyond each stage. journey.position shows where one record is. stage_map.unmapped should return nothing when you are done.Watch out for
- Journeys, stage maps and customer types are only active when signed in the production environment.
journey.move_recordis checked against the linked stage machine. An illegal move is refused.- A stage-map decision is saved only after a person confirmed it. It re-applies to the records already carrying that value (up to 1,000 per call).
- Removing a stage or type never rewrites records. They keep the old key and show as retired.
How the engines work together
GTM OS ships seven engines. Each one reads definitions you sign, does one job, and hands its result to the next. This chapter shows the chain, what starts each link, and the rules every engine shares. Chapter 9 then goes engine by engine.
Untested prompts. The prompts in this chapter were not run against a live instance. Operation names were checked against the 0.2.5 catalog. Run each once on a dev instance in Dry Mode before you rely on it.
What ships, and what does not
Goal: know exactly which engines exist in release 0.2.5, so you never promise a client something that is only on the roadmap.
| Engine | Package (Settings > Packages) | Console | Mode key | Job in one line |
|---|---|---|---|---|
| Score | stdlib/engine.score | Engines > Score (its Lead queue page), CRM > Hot list | crm_writeback | Turns signals and firmographics into a verdict, a tier and the reasons. |
| Outreach | stdlib/engine.outreach | Engines > Outreach, CRM > Sequences, CRM > Conversations | outreach (and meetings) | Drafts, sequences and sends email and LinkedIn steps through approved lanes. |
| Performance Ads | stdlib/engine.performance-ads | Engines > Performance Ads | perf_ads | Paces spend and moves budget toward your CAC or ROAS target. |
| SEO & GEO | stdlib/engine.search | Engines > SEO & GEO | seo_geo | Audits pages, tracks AI answers, plans and publishes content. |
| Data Quality (Merges) | stdlib/engine.identity-dq | Engines > Merges, Operate > Monitors | identity_dq | Finds duplicates, validates and normalizes, merges with undo. |
| Enrichment | stdlib/engine.enrichment | Engines > Enrichment | enrichment | Fills empty fields through a provider waterfall under a budget. |
| Signals | stdlib/engine.signals | CRM > Signals, CRM > Hot list | none | Records signals, gates them, detects intent surges. |
Signals has no engine page and no mode switch. It records and scores evidence and sends nothing out. Its one paid action, the daily signal scan, is stopped by the organization-wide Dry Mode.
Next to the seven engines sit surfaces that are not engines but that you configure the same way: Plays (Engines > Plays), Routing (Engines > Routing), the research agent (CRM > Research), CRM sync and source mappings, the adapter builder (CRM > Adapters) and AI chat. Chapter 7 covers routing and plays; chapter 10 covers data in.
The chain, from raw record to a reply
Goal: predict what a change in one engine does downstream.
- Data in. Import, CRM sync or a source mapping creates companies and contacts. These writes are marked
bulk, so event plays and agent triggers skip them unless they opt in. - Data Quality.
identity_dq.normalize_recordscleans domains, emails and casing. The duplicate scan proposes merges that follow your identity rule. - Enrichment. Fills empty fields (employee count, industry, country, email, phone). Fit scoring reads
employeeCount,countryCodeandindustry, so enrichment quality sets the ceiling for fit. - Signals. Signals arrive from the daily scan, intent ingest, your own site and mailbox, or
signals.record. The verification gate decides which ones count. Each new signal emitsgtm.signal.observed. - Score. Composite of fit (40 percent) and intent (60 percent) by default. A new qualification emits
gtm.scores.computed. - Handoff. When an account moves into qualified, the handoff routes an unowned account, creates a task and enrols a contact. The signal reaction can do the same on one strong signal.
- Outreach. The sequence cadence drafts the next step. A person approves it. The lane gate, the claims gate and the guardrails check it again at send time.
- Feedback. Replies are labelled. Unsubscribes, objections and hard bounces lower the account's intent score. Positive replies, meetings and won deals feed
score.learn, which proposes new weights.
SEO & GEO and Performance Ads run beside this chain. They share the same plumbing: the proof library and claims gate, your brand voice, the money definition (revenue truth and attribution), the guardrails and the approval gate.
score.explain_subject shows its reasons, the signals behind them and their decay. sequence.members shows where it sits in a sequence. Operate > Actions shows every draft and send with its outcome.What starts each engine
| Work | Starts | Where to change it |
|---|---|---|
| Re-score every stale account | Daily (score.rescan_organization), plus on a new signal, a verification change or a model change | Fixed schedule. Preview with score.rescan_preview. |
| Score learning | Weekly | score.learning_settings.set (human only) |
| Signal scan (looot research) | Daily; on demand with signals.scan_now | Feature flag signals.scan |
| Data quality scan | Daily (identity_dq.scan), on demand too | Feature flag identity_dq.thresholds, definition.data_monitor |
| Cadence rescan (next sequence step) | Hourly, and when a sequence is activated | definition.sequence |
| LinkedIn acceptance and reply polling | Hourly | Fixed |
| Ads allocator | Nightly at 02:00 UTC; perf_ads.reallocate on demand (once per hour) | definition.unit_economics |
| Ads campaign scan, SEO page audit | Scheduled batch (perf_ads.scan_all_campaigns, seo_geo.scan_all_pages), run by a scheduled play from your blueprint: daily-ads-pacing at 07:00 UTC (full-gtm and dtc-b2c blueprints) and daily-seo-audit at 05:30 UTC (full-gtm and seo blueprints) | Plays (Engines > Plays) |
| Tracked GEO and SERP visibility | Off until enabled; then every 24 hours by default | seo_geo.update_visibility_settings |
| CRM sync | Every 6 hours once a CRM of record is set | CRM sync settings |
| Enrichment | On demand, from a play, or from the research agent. Nothing enriches by itself. | Plays; requireMonthlyCap on the call |
Events you can build on
Goal: start a play or an agent when something happens, instead of on a timer.
Plays (definition.play, trigger kind event) and agent event triggers can start on these events. Each carries the fields you can filter on.
| Event | When | Fields to filter on |
|---|---|---|
gtm.signal.observed | A signal is recorded | signalId, kind, category, accountId, contactId, source |
gtm.signal.verification_changed | A person verifies or disputes, or a signal is retracted or restored | signalId, kind, toStatus, released, accountId, contactId |
gtm.scores.computed | An account gets a new qualification (not for an unchanged re-stamp or a dry run) | accountId, qualificationId, verdict, fitAvailable |
gtm.reply.received | A reply is recorded on any channel | replyId, channel, sourceSystem, contactId, accountId, dealId, campaignId, sourceCategory |
gtm.meeting.booked | A meeting appears on a connected calendar | meetingId, title, startsAt, accountId, source, sourceCategory |
gtm.record.created | A record is created | objectType, recordId, source, sourceCategory |
gtm.deal.stage_changed | A deal changes stage | recordId, fromState, toState |
gtm.journey.stage_entered | A record enters a journey stage | objectType, recordId, journeyKey, stageKey, category |
gtm.consent.changed | A contact's consent changes | contactId, channel, purpose, state |
gtm.dry_draft.produced | Dry Mode held a new draft | ledgerEntryId, engine, actionType, reason |
gtm.sync.completed | A sync finishes | monitorRunId, monitorKey, syncKind |
Events from bulk writers (import, CRM sync, enrichment, the projector) carry sourceCategory: "bulk". Plays and agent triggers skip them unless they set includeBulkSources. That is why a 10,000-row import does not start 10,000 plays.
The engines also write their own audit rows, called ledger action types. You read them in Operate > Actions and with actions.list. Examples: outreach.message_drafted, outreach.message_sent, perf_ads.campaign_changed, seo_geo.page_published, identity_dq.records_merged, enrichment.field_overwritten, crm.outcome_written. The reference lists the rest, and four more events (a mission step, a connection created, a connection tested, a sync finished).
Where each kind of knob lives
Goal: when a client asks "can we change X", know which route changes it and who has to sign.
| Kind of knob | How it changes | Who | Examples |
|---|---|---|---|
Definition (definition.*) | Claude proposes a ChangeSet in dev. A different person approves it, a human signs it, then you promote it. Engines read only signed versions. | Proposer, approver, signer. Needs a human | scoring_model, lane, sequence, brand_voice, unit_economics, identity_rule, binding |
| Definition, sign-exempt | Active on publish | Builder | data_monitor |
| One-step policy op | The op publishes and signs as the calling owner or admin. Agents can read it but not set it. | Owner or admin. Needs a human | enrichment.policy.set, signals.settings.set, score.learning_settings.set, policy.engine_modes.set, policy.guardrails.set, routing.member_settings.set |
| Feature flag | flags.set {key, value}, or Settings > Feature flags and Settings > Workflow automation | Owner or admin | signals.scan, signals.reaction, qualification.handoff, identity_dq.thresholds |
| Provider setting (not secret) | connections.set_provider_setting {provider, key, value} | Owner or admin | gsc/site_url, google_ads/customer_id, cms/target |
| Per-call input | An argument on one run of an op | Whoever runs it | maxUsd, estimate, maxEntities |
| Fixed in code | Cannot be changed from the console or by Claude. Say so honestly. | Needs a release | Ads overspend pace threshold, QA gate length floor, LinkedIn hard maxima |
The change loop is the same for every definition: ask Claude, let it validate (config.validate_changeset), have a different person approve, sign (gtm definition sign or Environments > Go live), promote to prod (config.promote), then verify. Chapter 5 walks through it and its failure modes.
Prompts for this chapter
See every engine at a glance
Show me the state of every engine: mode (dry or live), what is waiting for approval, and the last thing each one did. Read-only. Tell me which engines are still dry.- Claude does
- Calls
policy.engine_modes.get,actions.listandruntime.list_approvals, then summarizes. - You do
- Nothing. To take an engine live, do it in Settings > Safety.
Trace one account through the chain
Take the account acme.example. Show me its fit and intent scores and why, the signals behind them, whether it was handed off, and where its contacts sit in any sequence.- Claude does
- Calls
score.explain_subject,signals.list,sequence.members. Read-only. - You do
- Open CRM > Hot list > Why this score to compare.
Find out why nothing is being sent
Nothing is going out from outreach. Check, in order: engine mode, organization Dry Mode, guardrails and today's headroom, lane activation, sequence status and signed step plan. Report the first thing that blocks it.- Claude does
- Reads
policy.engine_modes.get,policy.guardrails.usage,lane.list,sequence.get,outreach.deliverability. - You do
- Fix the blocker. Going live and lane activation need a human.
Engine by engine
One page per shipped engine. Each page has the same parts: what the engine does, where it lives in the console, the knobs you can turn (with exact keys and defaults), the operations, the KPIs, the events, how Dry Mode and guardrails apply, how to extend it, prompts to give Claude, how to verify, and the mistakes to avoid. Work in this order on a new client: Enrichment and Signals (data), then Score, then Outreach, SEO & GEO and Performance Ads. Data Quality runs alongside from day one.
Every definition change below follows the loop in Where each kind of knob lives: Claude proposes in dev, a different person approves, a human signs, you promote, and engines read only the signed version. The prompts do not repeat that each time. Operation classes: Q read, D internal write, E customer-facing effect (always a proposal), M meta.
Untested prompts. The prompts in this chapter were not run against a live instance. Operation names were checked against the 0.2.5 catalog. Run each once on a dev instance in Dry Mode before you rely on it.
Score
Goal: a scoring model that matches the client's ideal customer and buying signals, with a reason on every verdict.
What it does
Score gives each account a number from 0 to 100, a verdict (Qualified, Needs review, Not qualified) and a priority tier. The default is a composite of two parts:
- Fit (40 percent): firmographic match from the signed
icp_fitmodel. - Intent (60 percent): the signals on the account. Each signal carries the weight of the rule that matches its kind. That weight fades with the signal's age, is multiplied by who observed it (your own systems count 1.5 times), and is lowered by negative replies and hard bounces. A hard disqualifier can cap the whole score.
Score also hosts CRM write-back, source sync and the learning loop. A new qualified account triggers the handoff: owner, follow-up task, sequence enrolment.
Where to find it
| Screen | Use it for |
|---|---|
| Engines > Score | Tabs KPIs, To score, Accounts, Rescan; the mode (Dry or Live) in the header; the Learning section with the last run, proposals, lift per criterion, calibration and rep disagreement. |
| CRM > Hot list | Accounts ranked by score. Why this score opens a drawer with the reasons and the sections "Caps and negatives", "Intent decay" and "Buying committee". |
| Lead queue (link in the Engines > Score header) | Rows show "capped", "negative reply / bounce" and band decay. Flag this score records a rep's disagreement. |
| Settings > Workflow automation | Switch the qualified-account handoff on or off. |
| Settings > Safety | Dry or live for the crm_writeback engine (score write-back to your CRM). |
What you can customize
Two scoring models do the work. definition.scoring_model key default is the intent model. Key icp_fit is the firmographic fit model.
| Knob | Key | Default | Notes |
|---|---|---|---|
| Intent rules | default: rules[].matchesKind, weight, optional condition, optional partyTiers | 13 rules, for example funding 30, hiring 25, acquisition or expansion 20, product launch 12, pricing or product page visit 10, risk or negative press -40, headcount decline -15 | A signal counts only if a rule names its kind. A kind with no rule adds nothing. |
| Verdict thresholds | qualifiedThreshold, needsReviewThreshold | 60 and 30 (fit model: 70 and 30) | Apply to the composite when verdictBasis is composite. |
| Fit and intent mix | composite.fitModelKey, fitWeight, intentWeight, intentCeiling | icp_fit, 0.4, 0.6, 100 | verdictBasis: composite or intent. |
| Decay per signal family (days) | composite.intentHalfLifeDays, intentHalfLifeByFamily | 30; buying 45, intent 14, engagement 21, product 21, lifecycle 60, negative 60 | A signal loses half its weight every half-life. |
| Who observed it | composite.partyTierMultipliers | first 1.5, second 1, third 1, unknown 1 (each 0 to 5) | First party is your own site, forms, mailbox, CRM. |
| Priority tiers | composite.tiers[] {id, label, minScore} | A Hot 75, B Warm 60, C Nurture 30, D Cold 0 | Console reads these labels. |
| Hard disqualifiers | disqualifiers[] {id, statement, cap, when} | One: score.default.dq.competitor, cap 0 | when takes relationshipStatuses, field {path, in or notIn}, signalKinds with withinDays, or suppressionReasons. The competitor rule does nothing until competitors are listed in setup. |
| Negative behavior | negatives | windowDays 180, halfLifeDays 60; unsubscribe 30, objection 10, wrong_person 5; hardBounce.penalty 15 | Points on the 0 to 100 intent scale. Labels come from your definition.reply_labels. |
| ICP fit rules | icp_fit: rules with matchesKind: [firmographic] and a CEL condition | Neutral, size only: 10 or more employees 40, 50 or more 40, 50 to 5,000 another 20 | Fit rules can read only employeeCount, countryCode, industry and marcomTeamSize. With no employee count, fit is unavailable and the composite rests on intent. |
| Buying committee | definition.committee_model key default: requiredRoles, championRole, recencyWindowDays, roles[] | none until you or a blueprint sets one | Read by score.committee_coverage and the segmented committee_coverage signal. |
| Segments and bands (advanced) | segmented: segments, signals, bands, bandDecay, repTouch, contactGrain | Not in the default model. The segmented template decays Hot to Warm after 3 days and Warm to Cold after 14 days without a rep touch. | Needed for contact-level scores (score.score_contact). |
| Handoff | Flag qualification.handoff | enabled true; route round robin over human sellers; task.dueInDays 2; enrol.maxContacts 1; skip customer, partner, former_customer; skip on risk or churn_risk within 30 days; an open deal blocks enrolment | route: false or enrol: false turns a step off. |
| Learning | score.learning_settings.set: autoPropose, outcome, modelKey, horizonDays, maturityDays | report only; outcome opportunity; model default; 60 and 60 | Learning never signs. Use 21 and 21 for fast outbound cycles. Human only |
Here is an icp_fit model for a client that sells to software companies with 50 to 500 staff in Germany, Austria and Switzerland:
kind: definition.scoring_model
key: icp_fit
spec:
scoreScaleId: icp.fit.0-100
qualifiedThreshold: 70
needsReviewThreshold: 30
rules:
- id: icp.size
statement: 50 to 500 employees
matchesKind: [firmographic]
weight: 50
condition:
expression: employeeCount >= 50 && employeeCount <= 500
variables:
employeeCount: int
- id: icp.region
statement: Germany, Austria or Switzerland
matchesKind: [firmographic]
weight: 30
condition:
expression: countryCode in ["DE", "AT", "CH"]
variables:
countryCode: string
- id: icp.industry
statement: Software
matchesKind: [firmographic]
weight: 20
condition:
expression: industry in ["Software", "SaaS"]
variables:
industry: stringOperations
score.score_account (D), score.score_contact (D), score.explain_subject (Q), score.explain_qualification (D), score.list_ranked (Q), score.account_tiers (Q), score.committee_coverage (Q), score.rescan_organization (D), score.rescan_preview (Q), score.kpis (Q), score.feedback (D), score.learn (D, proposes only), score.learning_report (Q), score.learning_settings.get (Q), score.learning_settings.set (M, human only), score.ingest_market_signals (D). The engine also hosts crm.write_outcome and crm.update_record (E, undoable), crm.sync and the source.* operations.
KPIs it publishes
score.kpis: accounts scored, tier distribution, hot now (top tier), next-move acceptance with its weekly trend, and score against deal outcome. Mission Control shows qualified_coverage. score.learning_report adds Brier score, reliability by tier and lift per signed version.
Events and ledger rows
- Emits
gtm.scores.computedfor a new qualification. Start a play on it. - On a move into qualified it queues
score.qualified_handoff, once per qualified episode. - Reads
gtm.signal.observedandgtm.signal.verification_changedthrough the signal reaction (see Signals). - Ledger rows for write-back:
crm.outcome_writtenandcrm.record_updated, each with an undo.
Dry Mode and guardrails
- Scoring is an internal write, so it runs in Dry Mode. Only write-back to your CRM (engine
crm_writeback) is held as a draft. - After a model change that alters the fingerprint, the first rescore never hands off. Nobody is routed, tasked or enrolled because of an upgrade rescore.
- Every handoff step runs through the normal gates: Dry Mode, approvals, the lane gate. A first message to a net-new contact still waits for a person.
- The signal verification gate decides which signals count. In Strict, unverified third-party signals never score.
How to extend it
- New signal kind. Record it with
signals.record(or a source mapping that targets signals), then add an intent rule whosematchesKindnames it. - Another field in fit. Fit rules read four fields. To score on anything else (technology, funding stage), score it as an intent rule on a signal, or use the segmented
account_fieldfeature. - Account-based tiers. Add a
committee_modeland the segmentedcommittee_coveragesignal for enterprise accounts. - Compile a client rule. The
/compile-ruleslash command turns a written scoring rule into definitions.
Prompts to give Claude
Set the client's ICP
Our client sells to software companies with 50 to 500 employees in Germany, Austria and Switzerland. Rewrite the icp_fit scoring model for that: size 50 points, region 30, industry 20. Keep the thresholds at 70 and 30. Validate it, then tell me what to approve.- Claude does
- Reads the current
icp_fitmodel withconfig.get, proposes a new version withconfig.propose_changeset, runsconfig.validate_changesetandconfig.compute_impact. - You do
- Needs a human A different person approves, a human signs, you promote.
Change how fast intent fades
Our sales cycle is short. Make web and topic intent fade with a 7-day half-life, buying triggers 30 days, and count our own website and mailbox signals 2 times. Show me a rescan preview before anything is signed.- Claude does
- Edits
composite.intentHalfLifeByFamily.intent(7),buying(30) andpartyTierMultipliers.first(2) in thedefaultmodel. Callsscore.rescan_previewafter it is signed indev. - You do
- Read the diff: moves into and out of qualified, tier churn, handoffs a real rescan would fire.
Disqualify a segment
Never qualify accounts whose country is RU or BY, and cap anything with a risk signal in the last 30 days at 20. Add these as disqualifiers to the default scoring model.- Claude does
- Adds two
disqualifiers[]:when.field {path: "countryCode", in: [...]}with cap 0, andwhen.signalKinds: [risk]withwithinDays: 30and cap 20. - You do
- Approve and sign. Check that Caps and negatives shows the cap on a test account.
Make a lost-deal reply hurt the score more
Raise the unsubscribe penalty to 50 and the hard bounce penalty to 25. Keep the 180-day window.- Claude does
- Edits
negatives.replyLabels[unsubscribe].penaltyandnegatives.hardBounce.penalty. - You do
- Approve and sign.
Let the model learn, safely
Run score learning in report-only mode for the outcome "meeting" with 21 day windows. Show me what it would change and the evidence. Do not propose yet.- Claude does
- Calls
score.learn {outcome: "meeting", horizonDays: 21, maturityDays: 21, propose: false}, thenscore.learning_report. - You do
- To let the weekly job propose on its own, set
score.learning_settings.set {autoPropose: true}yourself. Human only
How to verify the change took effect
score.score_account on a test account, then score.explain_subject. The policy version must read definition.scoring_model:default:vN (and definition.scoring_model:icp_fit:vN for fit), not score.default.v2 or score.icp_fit.default.v1, which are the built-in defaults. Open Why this score on the company page and check the reasons, the decay lines and any cap. Use score.rescan_preview before a model change goes live to see who moves.Common mistakes
- "My model is not used." It is unsigned, still in
dev, or you checked another environment. Engines read only the signed version in the call's environment. - A new signal kind scores nothing. Score counts only kinds that a rule names. Add the rule.
- Fit is blank. The account has no employee count (or none of the four fit fields). Enrich first. Fit is shown as unavailable, never as 0.
- Regional fit disappeared. The built-in fit model is size only. A regional ICP must be signed as your own
icp_fit. - Everything moved after an upgrade. The first rescore after a fingerprint change re-scores accounts once. Preview it first.
- Third-party signals do not count. A new workspace starts on Strict. Verify the signals, or set the gate (see Signals).
- Learning changes thresholds. It never does. It moves rule and segment weights only, at most 30 percent per run, and only with at least 30 accounts with and 30 without a criterion.
- A model may not list both a
bouncereply label andhardBounce. One bounce would count twice.
Outreach
Goal: sequences that send the right message on the right channel at the right time, and stop when they should, with a person approving every send.
What it does
Outreach drafts messages in your brand voice, runs sequences, and sends email and LinkedIn steps through activated lanes. Every send is a proposal (class E). A different person approves it, and every check runs again at execute time. It also labels replies, proposes next actions, and reports deliverability and what is working.
Where to find it
| Screen | Use it for |
|---|---|
| Engines > Outreach | Tabs KPIs, Pending sends, Contacts, Draft, Propose send, Advance cadence. Funnel tiles, a Deliverability panel, a sending lanes panel, What's working. |
| CRM > Sequences | Sequence list. A sequence has tabs Steps (plan editor with preview), Members, Messages, Replies. |
| CRM > Conversations | Every reply with its label and its proposed next action. |
| Operate > Needs a human, Approvals | Approve sends and next actions. The approval shows a Claims panel. |
| Settings > Brand & compliance, Proof library, Connected accounts, Safety | Brand safety rules, claims check mode, mailboxes, dry or live. |
What you can customize
| Knob | Key | Default | Notes |
|---|---|---|---|
| Lane: volume and cadence of touches | definition.lane: dailyCap, recentTouchWindowHours, openDealPolicy (block or allow), envelopeBudgetEur, windowDays | none set. The services blueprint uses 200, 336, block, 200, 30. | A send needs an activated lane. An un-laned send is refused. |
| Lane: checks | enforcedChecks: suppression, consent, mailbox_health, daily_cap, template_integrity, declared_params, recent_touch, open_deal, reply_stop | all checks run when the field is omitted | Listing checks narrows the set. Only list what you intend to keep. |
| Lane: auto-pause | bounceRatePauseThreshold, complaintRatePauseThreshold (0 to 1) | none. Blueprint: 0.04 and 0.001 | Rates show only after 10 sends. |
| Lane: sender identity | sendingDomain, dkimSelector, senderPhysicalAddress, temperature (cold or warm) | none | lane.activate checks SPF, DKIM and DMARC. In a prod-class environment it refuses unless all pass. |
| Lane: LinkedIn seat | linkedin.seat, invitesPerDay (max 40), invitesPerWeek (max 200), messagesPerDay (max 150), messagesPerWeek (max 700), inmail, warmup, noteMaxChars (max 300) | 20 and 100 requests, 50 and 250 messages; warm-up on (25, 50, 75, 100 percent over four weeks) | Per rolling 7 days. Note cap is 200 on a free LinkedIn account. |
| Sequence steps | definition.sequence: steps[] (1 to 20): channel, wait {amount 0 to 90, unit days or business_days}, sendWindow {start, end, days}, template or brief, variants[] (2 to 5, weights 1 to 100), personalisation | Without a step plan: one step every 3 days | Waits count in the recipient's timezone. Only a signed plan runs in prod. |
| Sequence exits | exits: reply, unsubscribe, bounce, manualStop (always on), meetingBooked, stageChange {objects, stages} | meetingBooked true | Checked at propose and again at execute. |
| LinkedIn step action | steps[].linkedin.action: message, connect, connect_then_message, inmail; note, acceptWait | message; accept wait 14 days, then next_step or exit | LinkedIn sending is off until an owner accepts the risk (broker.accept_high_risk). |
| Personalization depth | personalisation: auto, 1:1, 1:few, 1:many | auto: ABM tier (abmTier or tier on the account), else score band (hot 1:1, warm 1:few, cool or cold 1:many), else 1:1 | 1:many renders a template with merge fields and uses no model call. |
| Brand voice | definition.brand_voice key default: tone, vocabulary.preferred, avoid, examplePhrases, voiceSamples (up to 20), forbiddenClaims (up to 200) | none | Also used by SEO drafts. |
| Reply labels | definition.reply_labels: labels[] with phrases, patterns, senders, sentiment, priority, effects {sequence, consent, signalKind, openAsk, followUpDays}; modelAssist, minConfidence | modelAssist true, minConfidence 0.6 | A person's label always wins. |
| Who is never contacted | definition.suppression_rule (CEL criteria, reason), definition.compliance_policy (rules[]) | blueprint rules | Checked on every send. |
| Signal plays | outreach.signal_enroll: dailyCap, recentReplyDays, kinds, stepsKey, laneKey | 25 enrolments per organization per UTC day; skip anyone who replied in the last 90 days | Skips customers, partners and open opportunities. Needs an active sequence bound to a lane and a signed plan. |
| Claims check | Settings > Proof library > Claims check | block (an unbacked claim refuses the proposal) | Switch to "route to the approver" if you want the approver to acknowledge each claim. Close matches use a 50 percent wording threshold. |
| Org guardrails | policy.guardrails.set: emailsPerDay, newAccountsPerWeek, sendWindow, allowedChannels, customCaps | no limits | See Rules every engine shares. |
A three-step plan with an A/B test on the opener and an exit when a deal moves on:
kind: definition.sequence
key: cold_saas_v1
spec:
label: Cold outreach to SaaS founders
steps:
- key: opener
channel: email
wait: { amount: 0, unit: days }
personalisation: auto
variants:
- key: pain
weight: 50
template:
subject: "{{company|your team}} and onboarding time"
body: "Hi {{firstName|there}}, teams like {{company|yours}} tell us onboarding is the slow part. Worth a short call?"
- key: question
weight: 50
template:
subject: "Quick question, {{firstName|there}}"
body: "Hi {{firstName|there}}, how do you handle onboarding at {{company|your company}} today?"
- key: follow_up
channel: email
wait: { amount: 3, unit: business_days }
sendWindow: { start: "08:30", end: "11:30", days: [1, 2, 3, 4] }
brief: Add one approved proof point and repeat the small ask.
- key: close
channel: email
wait: { amount: 5, unit: business_days }
template:
body: "Hi {{firstName|there}}, I will leave it here. If timing changes, a reply is all it takes."
exits:
reply: true
unsubscribe: true
bounce: true
manualStop: true
meetingBooked: true
stageChange:
objects: [deal]Operations
Drafting and sending: outreach.draft_message (D), outreach.draft_personalized_message (D, spends model credits), outreach.send_message (E), outreach.undo_send_message (D, writes a retraction note; a sent email cannot be un-sent), outreach.sending_mailboxes (Q), outreach.advance_cadence (D), outreach.rescan_cadences (D), outreach.signal_enroll (D). Sequences: sequence.create, sequence.update, sequence.enroll (up to 500), sequence.set_member_status, sequence.message.resolve_uncertain (D); sequence.list, sequence.get, sequence.members, sequence.messages, sequence.replies (Q). Lanes: lane.activate (T, proposes), lane.list (Q). Replies: replies.ingest, replies.classify, replies.link, replies.next_action (D, proposes); replies.list, replies.get, replies.next_actions (Q); outreach.bounce_undo (D, human only). Reports: outreach.kpis, outreach.step_stats, outreach.learning_report, outreach.deliverability, outreach.linkedin_seats (Q). Activate a lane from a terminal with gtm lane activate <laneKey> --env <envId>.
KPIs it publishes
outreach.kpis: sends per week, human-approved share, reply rate by channel, bounces and complaints, meetings booked, cold against warm lanes, guardrail usage, and a funnel (sent, delivered, opened, replied, positive, meeting). Funnel rates stay empty under 20 sends. Opens are always flagged unreliable.outreach.step_stats: per sequence, step and A/B variant. Rates appear from 30 sends.outreach.learning_report: reply, positive-reply and meeting rate by angle, proof, persona, subject style, step and variant. A group needs 30 sends.- Mission Control:
reply_rate.
Events and ledger rows
- Consumes
gtm.signal.observed(signal plays),gtm.reply.received,gtm.meeting.booked. - Ledger rows:
outreach.message_drafted,outreach.message_sent. A reply with a confident label proposesoutreach.reply_next_action(book a meeting, snooze, referral). Unsubscribe and bounce propose nothing, because suppression already ran.
Dry Mode and guardrails
- Engine keys
outreachandmeetingsstart dry on a new organization. Dry drafts appear in Operate > Actions and emitgtm.dry_draft.produced. The Safety page shows how many dry-drafted contacts later replied. - Checks at propose time and again right before execute: Dry Mode, engine mode, claims gate, brand safety, suppression, consent, reply stop, quiet hours and the step's send window, guardrail caps, the lane gate, sequence exits and freshness.
- The first message to a net-new contact always needs a person.
- A cap, quiet hours or a LinkedIn limit holds the send. It returns to draft with the reason and is proposed again.
- A send whose outcome is uncertain is never retried. A person resolves it with
sequence.message.resolve_uncertain.
How to extend it
- A new cadence per motion. One signed
definition.sequenceper motion, one sequence per lane. - Signal plays. The outbound, full-gtm, sales-led and enterprise-abm blueprints ship three plays on
gtm.signal.observed(funding, hiring, intent). Copy one with/create-playfor a new signal kind. - New reply behavior. Add a label with its
effectsindefinition.reply_labels. - Other rails.
outreach.sending_mailboxeslists connected Google and Microsoft mailboxes, Gmail and Outlook through Composio, and outbound rails. Bind a sequence to the lane and mailbox you want.
Prompts to give Claude
Build a cadence with delays and exit rules
Create a 4-step email cadence for our outbound motion: day 0 opener, 3 business days later a proof follow-up (mornings Monday to Thursday only), 5 business days later a soft close, and 7 business days later a break-up note. A/B test two subject lines on the opener, 50/50. Exit on reply, unsubscribe, bounce, a booked meeting, or if the deal moves to any stage. Bind a new sequence to lane "outbound-email". Do not enrol anyone.- Claude does
- Proposes a
definition.sequenceand callssequence.createwithstepsKey(the sequence waits until the plan is signed). Does not callsequence.enroll. - You do
- Needs a human Approve and sign the plan. Activate the sequence after you have checked the Steps preview.
Set up and activate an email lane
Create an email lane called "outbound-email" for sending domain mail.example.com: daily cap 50, block contacts with an open deal, pause the lane at a 3 percent bounce rate or 0.1 percent complaints, cold temperature, and our postal address in the footer. Then propose activating it.- Claude does
- Proposes
definition.lanewithdailyCap: 50,openDealPolicy: block,bounceRatePauseThreshold: 0.03,complaintRatePauseThreshold: 0.001,temperature: cold,senderPhysicalAddress. After signing, callslane.activate. - You do
- Needs a human Sign the lane, publish the DNS records, approve activation, bind a mailbox.
Cap volume across the whole organization
Limit outreach to 150 emails a day across all engines, 40 new accounts a week, and only Tuesday to Thursday between 09:00 and 16:00 in the recipient's time. Show me today's headroom afterwards.- Claude does
- Reads
policy.guardrails.get, then prepares the full spec forpolicy.guardrails.set. Agent keys cannot set it, so Claude gives you the exact spec. Readspolicy.guardrails.usageafter. - You do
- Human only Set it in Settings > Safety.
Change the brand voice
Update our brand voice: direct and warm, no exclamation marks, prefer "teams" over "users", avoid "revolutionary", "synergy" and "game-changing". Forbid the claim "guaranteed results". Add the two voice samples I paste below.- Claude does
- Edits
definition.brand_voicekeydefault:tone,vocabulary,forbiddenClaims,voiceSamples. - You do
- Approve and sign. Draft a test message and check the tone.
Teach it a new reply label
Add a reply label "budget_next_quarter" with neutral sentiment, recognized by the phrases "next quarter", "after the budget cycle" and "revisit in". Effect: stop the sequence for that person and open a task for the owner due in 60 days titled "Follow up on budget".- Claude does
- Adds a label to
definition.reply_labelswithsentiment: neutral,effects.sequence: stopandeffects.openAsk {title, dueDays: 60}. - You do
- Approve and sign. Send a test reply through
replies.ingestin dev and checkreplies.getshows the label.
How to verify the change took effect
sequence.get returns the signed plan the sequence follows. sequence.members shows each member's next step, exit and any hold with its reason. lane.list shows the lane as activated and sendable. Draft one message and propose one send to an internal address. The approval lists every check. In Dry Mode the result is a drafted row in Operate > Actions instead of a send. outreach.deliverability shows the lane against its pause threshold.Common mistakes
- Members wait at step 1. The step plan is unsigned in prod, or the sequence is not active, or it has no lane. The roster says which.
- "lane_required". A send needs a lane that is signed, activated and sendable. Activation needs a different approver.
- Narrowing
enforcedChecksby accident. Leaving a check out turns it off. Omit the field to keep them all. - A claim blocks the send. Stats in copy need active proof in the Proof library. Add the proof, or rewrite the claim.
- Nothing sends after go-live. The engine is still dry, the first message is to a net-new contact (it always waits), quiet hours are holding it, or a cap is reached. Check
policy.guardrails.usage. - Two paths enrol the same person. If signal plays run, set
signals.reactiontoenrol: false. Otherwise the second path skips within_another_sequence. - LinkedIn refuses to send. An owner has not accepted the LinkedIn risk, the seat is not resolved, or a seat cap is reached.
- Open and click tracking. Resend tracking is a domain setting. Turn it on only for a domain whose recipients consented. Opens never feed suppression.
Performance Ads
Goal: ad spend that follows the client's unit economics, with budget moves proposed on real customers and approved by a person.
What it does
Performance Ads syncs spend and results from Google Ads and Meta Ads, paces each campaign against its schedule, and judges it on real CAC and real ROAS: customers and revenue from your own revenue truth, not the platform's conversion count. Every night an allocator proposes small, budget-neutral moves from campaigns that cost too much per customer to ones that cost less. Every change is a real-money proposal (class E).
The engine does nothing on its own until you set a target. Without targetCac, targetRoas or maxPaybackMonths, pacing follows the schedule only and the allocator runs but proposes nothing, and says why.
Where to find it
| Screen | Use it for |
|---|---|
| Engines > Performance Ads | Tabs Pacing, Campaigns, Scan, Update, Undo. Tiles for spend, conversions, real ROAS, real CAC, MER, aMER, ACoS and CAC payback. A Platform vs real table. An Allocator section with the last run, every move with its evidence and approval link, campaigns left alone and why, Preview and Run now. A Settings section with your targets. |
| Settings > Money | Revenue truth and the attribution window and model. |
| Settings > Safety | Dry or live for perf_ads, and the daily ad spend guardrail. |
| Integrations > Connections | Google Ads and Meta Ads (native or through Composio). |
What you can customize
| Knob | Key | Default | Notes |
|---|---|---|---|
| The target | definition.unit_economics key default: targetCac, or targetRoas, or maxPaybackMonths (max 120) with grossMargin and monthlyRevenuePerCustomer | none | The older cac field is not a target. |
| Other economics | ltv, newCustomerAov, costOfDelivery, repeatPurchaseRate, amerCurve[], currency | none | Feed the first-order profit gate and the spend ceiling. |
| Attribution | definition.money: attribution.model (equal_split, position_based, first_touch, last_touch, time_decay), windowDays (1 to 365), positionWeights, halfLifeDays | equal split | Applied when a report is read. Switching recomputes every report. |
| Revenue truth | definition.money.revenueTruth: crm_deals, or a warehouse or billing object | CRM deal amounts | CAC and ROAS are only as good as this. |
| Allocator tolerance band | perf_ads.reallocate {band} | 0.2 (0.05 to 0.5) | A campaign is "worse" or "better" only outside the band. |
| Allocator step | maxStepPct | 0.2, and 0.2 is the hard maximum | Per campaign per run, and per UTC day from the start-of-day budget. |
| Evidence before judging | minSpend, minConversions, minCustomers, minCampaignAgeDays, windowDays | 250, 5 (or 2 customers), 2 customers to receive budget, the attribution window, the larger of 30 days and the attribution window | Per-run inputs on a manual run. The nightly run uses the defaults. |
| Cool-down | coolDownDays | 7 (0 to 60) | A campaign changed within this time is not moved. |
| Daily budget ceiling | policy.guardrails.set: adSpendPerDay {amount, currency} | no limit | The allocator moves nothing while projected daily budget is above it. Human only. |
| Channels allowed | guardrails allowedChannels (include ads) | no restriction | |
| Ad accounts | connections.set_provider_setting: google_ads/customer_id, google_ads/login_customer_id | none | Meta needs no account setting. In live mode the connector reads only this. |
| Manual change cap | perf_ads.update_campaign | An increase is capped at 2 times the current budget | A larger value is rejected. |
| Pacing thresholds | Fixed in code: overspend pace 1.3, underspend pace 0.3 | fixed | Not a setting. |
Example target for a client that can pay at most 250 EUR to win a customer:
kind: definition.unit_economics
key: default
spec:
label: Client unit economics
currency: EUR
grossMargin: 0.7
targetCac: 250
ltv: 2400Set one basis: targetCac, or targetRoas, or maxPaybackMonths with its two companions.
If you set more than one, the engine steers on CAC: an explicit targetCac, otherwise the CAC a payback limit implies. It uses targetRoas only when no CAC target exists.
Operations
perf_ads.sync_performance (D), perf_ads.get_pacing_recommendation (Q), perf_ads.get_campaign_performance (Q), perf_ads.scan_campaign (D), perf_ads.scan_all_campaigns (D), perf_ads.update_campaign (E), perf_ads.undo_campaign_change (E), perf_ads.reallocate (D, queues the allocator), perf_ads.reallocation_plan (Q, previews, proposes nothing), perf_ads.allocator_runs (Q), perf_ads.kpis (Q).
KPIs it publishes
perf_ads.kpis: spend, conversions, CPA, ROAS on real customers, real CAC, platform against real, MER, aMER (new-customer revenue over spend), ACoS, payback against your economics, and the open moves. Metric definitions perf_ads_spend_total, perf_ads_conversions_total, perf_ads_campaign_count, perf_ads_outcome_freshness. Mission Control shows spend_total. Only live synced rows count. Demo data and fixture rows never do.
Events and ledger rows
- Ledger row
perf_ads.campaign_changed, withperf_ads.undo_campaign_changeas its undo. - The allocator is a nightly job at 02:00 UTC. Each of its moves is its own approval ("Reallocate ad budget (decrease / increase)") with the real CAC, platform CPA, customers, target and paired campaign.
- No domain event is emitted. Start plays from a schedule or from
gtm.deal.stage_changed.
Dry Mode and guardrails
- Engine key
perf_ads. In Dry Mode the allocator drafts its plan and proposes nothing. - Allocator bounds: at most 20 percent per campaign per run, budget-neutral within one currency, never a paused campaign, a demo-data campaign, a campaign with a change already pending, or a campaign read only through Composio (no native write path; the recommendation is reported, not proposed).
- A campaign is not judged before it is as old as your attribution window, and never called worse on zero customers.
- After an executed change the campaign is left alone for 7 days.
- "Run now" works once per hour. A repeat returns the same run.
- An approved move is refused at execution if the campaign's budget changed after the proposal.
- Approving a decrease without its paired increase lowers total spend. Approving an increase alone raises it. The approval shows what each choice does.
How to extend it
- Another platform. The engine runs on Google Ads and Meta Ads. A platform your connectors reach only through Composio is read-only here.
- Tag your ads. A paid visit is credited to the campaign named in
utm_campaign: the platform campaign id or the exact campaign name. Untagged traffic leaves real CAC empty, with the reason. - A play on a schedule. Run
perf_ads.scan_all_campaignsfrom a play so campaigns are scanned and proposals appear without anyone clicking. - Use
/tune-economics. The slash command walks through the economics with the client's numbers.
Prompts to give Claude
Set the CAC target
The client can pay at most 250 EUR to acquire a customer. Gross margin is 70 percent. Set the unit economics target to a CAC of 250 EUR and show me what the allocator would do right now, without proposing anything.- Claude does
- Proposes
definition.unit_economicskeydefaultwithtargetCac: 250,grossMargin: 0.7,currency: EUR. After signing, callsperf_ads.reallocation_plan. - You do
- Needs a human Approve and sign the economics.
Cap daily ad budget
The client's total ad budget must never exceed 400 EUR a day. Give me the exact guardrail spec to set, and tell me what the allocator does if we are already above it.- Claude does
- Reads
policy.guardrails.getand preparesadSpendPerDay: {amount: 400, currency: "EUR"}in the full spec. It cannot set it. - You do
- Human only Set it in Settings > Safety.
Make attribution fair for a long sales cycle
Our customers take about 45 days from first ad click to purchase. Set the attribution window to 45 days with a position-based model (40 percent first touch, 40 percent last touch). Tell me how real CAC will change.- Claude does
- Edits
definition.money:attribution.windowDays: 45,model: position_based,positionWeights {first: 0.4, last: 0.4}. - You do
- Approve and sign. Reports recompute at once. Nothing stored is rewritten.
Review pending budget moves
Show me the last allocator run: each move with its evidence, which campaigns it left alone and why, and any approval still waiting.- Claude does
- Calls
perf_ads.allocator_runsandperf_ads.kpis. Read-only. - You do
- Needs a human Approve or reject each move in Approvals. A different person than the proposer.
How to verify the change took effect
perf_ads.sync_performance on a live campaign returns real spend (rows are stamped live). perf_ads.reallocation_plan returns a plan status of moves, no_moves or no_economics, with the target and basis it read. no_economics means the economics are not signed in this environment. On the Performance Ads page, each campaign shows real CAC and a badge against your target.Common mistakes
- "The allocator never proposes anything." No target is signed, the campaigns are too young or too small, or you are in Dry Mode. The run says which.
- Real CAC shows a dash. Paid visits do not carry
utm_campaignthat names a tracked campaign, or revenue truth is not set. The page gives the reason. - Spend looks low after the upgrade. Rows synced before 0.2.5 E3 had no live stamp. Re-run
perf_ads.sync_performanceover the dates you need. Nothing is deleted. - The daily budget is above the guardrail. The allocator moves nothing until it is below it, or the guardrail is in another currency.
- Setting more than one target. Set one basis (CAC, ROAS or payback) so the verdict is unambiguous.
- Expecting a Composio-only campaign to move. It is read-only: the engine reports the budget it recommends but does not propose it.
SEO & GEO Search Console required
Goal: know how the client shows up in Google and in AI answers, and run a content loop from topic to published page.
What it does
The engine audits tracked pages (technical SEO, GEO readiness, Google's index status), tracks the prompts you care about on AI answer engines and the keywords you care about on Google, groups demand into topic clusters, runs a content factory (brief, draft, QA gate, approval) and publishes an approved draft to a CMS. Every publish is a proposal that a different person approves. Google Search Console is required. An organization that does not use SEO does not need it.
Where to find it
| Screen | Use it for |
|---|---|
| Engines > SEO & GEO | Tabs Overview, Visibility (Search Console totals, top queries and pages), GEO (AI-answer visibility), Keywords, Clusters, Factory, Fix queue, Publish queue, Refresh calendar, Settings, Pages (the page workbench). The Factory item has a Publish tab. |
| Settings > Proof library | The proof the claims gate and the content briefs cite. |
| Integrations > Connections, Settings > Connected accounts | Google Search Console, GA4, Ahrefs (through Composio), your CMS. |
What you can customize
| Knob | Key | Default | Notes |
|---|---|---|---|
| Search Console property | connections.set_provider_setting {provider: "gsc", key: "site_url"} (or gtm apply --gsc-property) | none | sc-domain:example.com or https://www.example.com/. Required. seo_geo.gsc_properties lists what the connection can read. |
| Tracked prompts and keywords | seo_geo.create_tracked_prompt: kind (ai_prompt or serp_keyword), text, brand, brandAliases (up to 20), competitors (up to 25), targetUrls (up to 25), engines, country, language, active | all engines, US, en, active | Engines: chatgpt, perplexity, gemini, copilot, google_ai_overview. A serp_keyword needs a target URL and takes no engines. The same kind and text twice is a conflict. |
| Scheduled tracked visibility | seo_geo.update_visibility_settings: enabled, cadenceHours (1 to 720), rankDropThreshold, visibilityDropThreshold (0 to 1), maxRunCostUsd | off; 24; 3 positions; 0.15; 1 USD | Paid. Enabling on a live instance needs Search Console. A run stops when it reaches the cap. |
| Cluster build | seo_geo.build_clusters: method (auto, serp_overlap, embedding, lexical), embeddings (skip or use), maxCostUsd, minImpressions, maxItems, days | auto, skip, 0.25 USD, 5, 500, 28 | Free unless you opt into embeddings. seo_geo.cluster_sources shows the estimate first. |
| Brand voice and forbidden claims | definition.brand_voice key default | none | Drafts are written in it, and QA fails on a forbidden claim or avoided word. |
| Claims check mode | Settings > Proof library > Claims check | block | In route-to-approver mode QA only warns and the approver acknowledges at publish. |
| CMS target | Provider cms: target, site_url, webflow_collection_id, webflow_field_map, wordpress_com_site_id, webhook_endpoint_id | none | Targets: webflow (Composio), wordpress_com (drafts only), webhook (signed, also the route for self-hosted WordPress). seo_geo.cms_targets shows each target's honest state. |
| Refresh rules | Provider cms: refresh_traffic_drop_pct, refresh_age_days | 20 and 180 | Also fixed: ranking down 3 places, or a tracked AI prompt no longer cites the page. |
| Keyword data | seo_geo.refresh_keyword_data | off until run | Needs looot. Without it the Keywords tab still shows Search Console and Ahrefs data. |
| QA gate | Fixed in code | reading ease 30 or more, 30 words or fewer per sentence, 150 words minimum, a title, no placeholder left | Not a setting. |
Operations
The engine has 44 operations. Pages and audits: seo_geo.create_tracked_page, seo_geo.run_audit, seo_geo.audit_page, seo_geo.scan_all_pages (D); seo_geo.get_recommendation, seo_geo.get_readiness_view, seo_geo.get_page_performance, seo_geo.fix_queue (Q). Site and AI visibility: seo_geo.run_visibility, seo_geo.create_tracked_prompt, seo_geo.update_tracked_prompt, seo_geo.delete_tracked_prompt, seo_geo.update_visibility_settings (D); seo_geo.get_visibility, seo_geo.list_tracked_prompts, seo_geo.get_visibility_settings, seo_geo.list_visibility_alerts, seo_geo.gsc_properties, seo_geo.kpis (Q). Keywords: seo_geo.keyword_opportunities (Q), seo_geo.refresh_keyword_data (D). Clusters and factory: seo_geo.build_clusters, seo_geo.add_content_idea, seo_geo.create_brief, seo_geo.draft_page, seo_geo.update_draft, seo_geo.qa_draft, seo_geo.approve_draft (human only, not the author), seo_geo.move_content, seo_geo.mark_refresh_due (D); seo_geo.clusters, seo_geo.get_cluster, seo_geo.cluster_sources, seo_geo.content_item, seo_geo.factory, seo_geo.refresh_candidates, seo_geo.refresh_calendar (Q). Publishing: seo_geo.publish_content_change, seo_geo.undo_content_change, seo_geo.publish_to_cms, seo_geo.unpublish_from_cms (E); seo_geo.publish_queue, seo_geo.publish_preview, seo_geo.cms_targets (Q).
KPIs it publishes
seo_geo.kpis: pages tracked and live, indexed share, organic clicks and impressions, AI referral visits (GA4), AI citation share per answer engine, each with its previous value and trend, plus publishing KPIs (time to publish, time to index, citation gain after publish). Metric definitions: search_organic_traffic, search_avg_rank, search_geo_readiness, search_tracked_pages, search_ai_share_of_voice, search_ai_citation_rate, search_ai_mention_rate, search_serp_avg_rank, search_ai_referral_sessions, search_outcome_freshness. Visits from AI assistants (ChatGPT, Perplexity, Gemini, Copilot, Claude and others) are credited to SEO & GEO.
Events and ledger rows
- Ledger rows
seo_geo.page_published(undo:seo_geo.undo_content_change) andseo_geo.page_published_to_cms(undo:seo_geo.unpublish_from_cms). - Search alerts for keyword rank drops and AI-visibility drops, read with
seo_geo.list_visibility_alerts. - Content lifecycle states: idea, brief, draft, qa_passed, approved, published, refresh_due.
Dry Mode and guardrails
- Engine key
seo_geo. A dry engine audits and drafts but publishes nothing. - The copy published is the approved draft, bound to its hash. An edited draft is refused. A retry never creates a second page.
- The claims gate and brand safety run at propose and again at execute. A different person than the author approves the draft, and a different person approves the publish.
- Every tracked acquisition goes through the metered capability broker with a per-run cap. A cluster build with embeddings is estimated first, refused over its cap and reserved on your org budget.
- Without a connected CMS the answer is "not connected". There is never a fake publish.
How to extend it
- New CMS. Use the signed webhook target to reach any CMS you can write an endpoint for.
- New markets. Add tracked prompts per country and language.
- Plays. Schedule
seo_geo.scan_all_pagesandseo_geo.mark_refresh_duefrom a play so fixes and refreshes queue themselves. - Competitor tracking. Name competitors on each tracked prompt so share of voice counts them.
Prompts to give Claude
Start tracking AI answers for a client
Track these prompts on ChatGPT, Perplexity and Google AI Overviews for brand "Acme", also written "Acme Corp": "best CRM for small agencies", "how to automate lead scoring", "Acme alternatives". Competitors: Globex and Initech. Our domain is example.com. Country US, English. Do not turn on the schedule yet.- Claude does
- Calls
seo_geo.create_tracked_promptthree times withkind: ai_prompt,brand,brandAliases,competitors,targetUrls: ["example.com"]andengines: [chatgpt, perplexity, google_ai_overview]. Lists them back withseo_geo.list_tracked_prompts. - You do
- Nothing. Tracking costs money only when a run happens.
Turn on weekly tracked visibility with a spend cap
Turn on scheduled visibility tracking every 168 hours. Alert me if any keyword loses 5 positions or an engine's share of voice falls by 20 percent. Stop a run at 3 dollars.- Claude does
- Calls
seo_geo.update_visibility_settings {enabled: true, cadenceHours: 168, rankDropThreshold: 5, visibilityDropThreshold: 0.2, maxRunCostUsd: 3}. - You do
- Make sure Search Console is connected first. On a live instance the call is refused without it.
Set the Search Console property
List the Search Console properties our connection can read, then select the one for example.com.- Claude does
- Calls
seo_geo.gsc_properties, thenconnections.set_provider_setting {provider: "gsc", key: "site_url", value: "sc-domain:example.com"}. - You do
- Confirm the property in the list. An owner or admin must be the one the key acts for.
Plan content from real demand
Show me what a cluster build would read and what embeddings would cost. Then build clusters from the last 28 days without embeddings, and give me the top 5 clusters that are a gap or cannibalised, with a brief for the best one.- Claude does
- Calls
seo_geo.cluster_sources,seo_geo.build_clusters {embeddings: "skip"},seo_geo.clusters,seo_geo.create_brief. - You do
- Review the brief. A person other than the author approves the draft later.
Publish an approved draft
Preview publishing the approved draft "crm-for-agencies" to Webflow. Show me exactly what would be sent and every reason it cannot go yet. If it can, propose it.- Claude does
- Calls
seo_geo.publish_preview, thenseo_geo.publish_to_cms {contentAssetId, target: "webflow"}(class E). - You do
- Needs a human A different person approves in Approvals. The Claims panel lists any claim to acknowledge.
How to verify the change took effect
gtm doctor shows the Google Search Console line OK. seo_geo.create_tracked_page then seo_geo.audit_page on a real URL returns findings. seo_geo.get_visibility_settings shows your cadence, thresholds, cap and the next run time. seo_geo.cms_targets shows your CMS as ready. seo_geo.refresh_calendar shows when each page is next audited.Common mistakes
- "NOT_CONFIGURED" on visibility or clusters. Search Console is not connected, or no property is set. Fixture rows are never clustered.
- Tracked runs cost more than expected. Every prompt on every engine is a paid lookup. Set
maxRunCostUsdand keepenginesnarrow. - WordPress.com "published" nothing. WordPress.com is draft only. Use the signed webhook for a self-hosted WordPress.
- QA keeps failing. Any edit voids QA and any approval. Run QA again after editing.
- A claim blocks publish. A stat in the page needs active proof. A customer whose proof is not public can never be named.
- Empty indexed share. It fills after a page is audited following the upgrade. Run Audit all now in the Refresh calendar.
- Duplicate tracked prompt. The same kind and text twice is a conflict. Edit with
seo_geo.update_tracked_prompt.
Data Quality (identity and merges)
Goal: one record per real company and person, with clean fields, and a clear rule for which value wins when two records merge.
What it does
The engine validates records, normalizes domains, emails, casing and whitespace, scans for duplicates by your identity keys, and merges duplicates through an approval, with unmerge. A daily scan records quality as monitor runs and alerts the owner when a threshold is breached. It surfaces problems and proposes. It never merges on its own.
Where to find it
| Screen | Use it for |
|---|---|
| Engines > Merges | Mode, merges waiting for approval, recent merges with undo, the identity rule. Its Data quality tab (also /data-quality) shows the quality score, field fill rate and Duplicate candidates, and has the Merge rules card. |
| Operate > Monitors | Data monitors, their runs, lane resume. |
| Operate > Needs a human | Uncertain identity matches wait here. |
| Settings > Safety | Dry or live for identity_dq (Duplicate merges). |
What you can customize
| Knob | Key | Default | Notes |
|---|---|---|---|
| Identity keys and their role | definition.identity_rule per objectType (gtm.account, gtm.contact): rules[] {key, role}; role is authoritative, corroborating or forbidden | Built-in values when no rule is signed (the scan says so). Blueprints: accounts domain (authoritative), name, phone, linkedinUrl (corroborating) | A forbidden or unlisted key never matches. Blueprints applied before 0.2.5 list only domain and name (accounts) or email and name (contacts), so the scan no longer matches on phone or LinkedIn for them. Add them back. |
| Match weights | matching.weights | same_email 0.95, same_linkedin 0.95, same_phone 0.9, same_domain 0.8, same_name_and_country 0.7, similar_name 0.68, same_name 0.5 | Only the reasons you name change. |
| Minimum confidence | matching.minConfidence (0 to 0.99) | 0 (every matched pair is proposed) | Raise it to cut noise. |
| Name similarity floor | matching.nameSimilarity (0.5 to 1) | 0.82 | Used for the similar_name reason. |
| Which value wins on a merge | survivorship.mergeDefault and survivorship.mergePerProperty. Strategies: keep_survivor, most_recent, highest_confidence, first_write_wins, longest_non_empty, source_priority (with order), confidence_weighted, manual | Keep the survivor's values and fill only its empty fields | manual blocks approval until a person picks the value. Merge-only rules never change how incoming data is projected. |
| Which value wins on incoming data | survivorship.default, survivorship.perProperty | most_recent in the blueprints | Since 0.2.5 a perProperty rule also decides a merge unless a merge-only rule exists for the field. |
| Quality alert thresholds | Feature flag identity_dq.thresholds: maxDupRate, minMeanQuality, maxStaleRate | 0.05, 0.5, 0.8 | Set with flags.set. A breach marks the daily run breach and notifies the owner. |
| Staleness window and scan size | staleDays and limit inputs on identity_dq.quality_report; limit on the scans | 30 days; 1,000 records (max 5,000) | Per-call inputs. |
| Data monitors | definition.data_monitor: type (freshness, volume, schema_drift, fill_rate, conflict_rate, resolution_queue, effect_failure_rate, reconciliation, bounce_rate, complaint_rate), objectType, property, lane, intervalMinutes, windowMinutes, warnThreshold, breachThreshold, minSample, autoPauseLane | blueprint monitors | Sign-exempt: active as soon as it is published, even in prod. |
| Normalization rules | Fixed in code: domain, email, casing, whitespace | fixed | Never blanks a value or changes its meaning. |
Example: accounts match on domain, and a person picks the industry when two records disagree:
kind: definition.identity_rule
key: gtm.account
spec:
objectType: gtm.account
rules:
- { key: domain, role: authoritative }
- { key: name, role: corroborating }
- { key: phone, role: corroborating }
- { key: linkedinUrl, role: corroborating }
survivorship:
default:
strategy: most_recent
mergeDefault:
strategy: keep_survivor
mergePerProperty:
industry:
strategy: manual
employeeCount:
strategy: highest_confidence
matching:
minConfidence: 0.7
nameSimilarity: 0.9Operations
identity_dq.scan_duplicates (Q), identity_dq.preview_merge (Q), identity_dq.validate_records (Q), identity_dq.quality_report (Q), identity_dq.merge_rules.get (Q), identity_dq.merge_rules.propose (D, builds a ChangeSet), identity_dq.flag_invalid_records (D), identity_dq.normalize_records (D, reversible), identity_dq.undo_normalization (D), identity_dq.backfill_domains (D, free, from contacts' work emails), identity_dq.enrich_accounts (D), identity_dq.scan (D, on-demand scan), identity_dq.merge_records (E), identity_dq.revise_merge (E, people only), identity_dq.unmerge_records (E).
KPIs it publishes
identity_dq.quality_report: fill rate per field, duplicate rate, staleness and mean quality score. The daily scan stores one monitor run per object type (identity_dq.scan:gtm.account, identity_dq.scan:gtm.contact) with the change since the last run, so Operate > Monitors shows the trend.
Events and ledger rows
- Ledger rows
identity_dq.records_merged(undo: unmerge),identity_dq.records_unmerged,identity_dq.records_normalized(undo:identity_dq.undo_normalization). - A threshold breach sends the owner a
dq_alertnotification, once per object type per day. - Writes from the merge are recorded in field history. A value a person picked is recorded as the approver's edit, so a later CRM sync does not overwrite it.
Dry Mode and guardrails
- Engine key
identity_dq. A merge is a proposal in every mode. A different person approves it. A pick on the approval card is re-proposed as a new approval, and a third person approves it. - If the survivor changed after the proposal, the merge refuses and changes nothing.
- Undo restores the survivor's earlier values and the duplicate, except for fields edited after the merge. The result lists what was restored and what was kept, and why.
- Normalization is an internal write. It is not behind an approval. It is idempotent and reversible.
How to extend it
- New object type. Identity rules cover accounts and contacts for the duplicate scan. Deals can be merged by hand with
identity_dq.merge_records. - More monitors. Add
definition.data_monitorinstances per lane (bounce rate, complaint rate) or per property (fill rate onindustry). - Run it after every import. A play on
gtm.sync.completedcan run the scan and the quality report.
Prompts to give Claude
Clean a fresh import
We just imported 4,000 companies. Run the quality report for accounts, normalize the records, backfill missing domains from contact emails, scan for duplicates, and show me the top 20 duplicate pairs with their match reasons. Do not merge anything.- Claude does
- Calls
identity_dq.quality_report,identity_dq.normalize_records,identity_dq.backfill_domains,identity_dq.scan_duplicates. Reports the numbers before and after. - You do
- Nothing. Normalization can be reversed with
identity_dq.undo_normalization.
Change the matching policy
Our duplicates are mostly the same company under different names. Match accounts on domain first, then LinkedIn URL, then name. Use a name similarity of 0.9 and ignore pairs with confidence under 0.7. When two records disagree on industry, make a person choose. For employee count, keep the value with the highest confidence.- Claude does
- Calls
identity_dq.merge_rules.get, thenidentity_dq.merge_rules.proposewithmatching {nameSimilarity: 0.9, minConfidence: 0.7}and the per-field strategies. - You do
- Needs a human Approve, sign and promote. Nothing applies until it is signed in prod.
Propose one merge and show the conflicts
Preview merging acme-inc.example into acme.example. Show every field that conflicts, how each one would be decided and why. If it looks right, propose the merge.- Claude does
- Calls
identity_dq.preview_merge, thenidentity_dq.merge_records(class E). - You do
- Needs a human A different person approves in Approvals. They can pick a value on the card.
Alert me when quality drops
Alert the owner when more than 3 percent of companies are in a duplicate pair, or when the mean quality score falls below 0.6. Also add a monitor for the fill rate of industry on accounts, warning at 70 percent and breaching at 50 percent, checked daily.- Claude does
- Sets the flag
identity_dq.thresholds {maxDupRate: 0.03, minMeanQuality: 0.6}withflags.setand publishes adefinition.data_monitorof typefill_ratewithproperty: industry,intervalMinutes: 1440. - You do
- Nothing for the monitor (sign-exempt). The flag needs an owner or admin.
A fill_rate monitor is a floor. It warns when the fill rate is at or below warnThreshold and breaches at or below breachThreshold, so set warn above breach (for example warn 0.7, breach 0.5).
How to verify the change took effect
identity_dq.merge_rules.get shows the effective keys, weights, threshold and each field's strategy with its source (signed or built-in). identity_dq.scan_duplicates returns a rules block that says which rule applied and its version. Run identity_dq.quality_report before and after an import or CRM sync and compare. The Monitors page shows the daily run as healthy or breach.Common mistakes
- Duplicates by phone or LinkedIn stopped showing up. Your signed identity rule lists only domain and name. Add them as corroborating keys.
- Wrong merges. Do not make a weak key authoritative. A shared email domain (a free mailbox provider) is a weak match.
- Approval is stuck. A field set to
manualneeds a person to pick its value on the card. - Merge refuses to run. The survivor changed after it was proposed. Propose it again.
- "Why did my edit come back after a sync?" Fields you edit by hand are kept. A value the merge rules decided can still be changed later by a better source.
- Expecting normalization to need approval. It does not. Undo it with
identity_dq.undo_normalization.
Enrichment
Goal: fill the empty fields the other engines need, at a price the client agreed to, and keep them fresh.
What it does
Enrichment fills empty fields on companies and contacts through a waterfall: for each field it tries the capabilities that can fill it, in order, and stops at the first good value. Each value is stamped with when it was verified, how long it stays fresh and a confidence. A stale value that enrichment wrote is re-checked later. A newer enriched value replaces an existing one only through an approval, with undo. Spend is real, so always estimate first.
Where to find it
| Screen | Use it for |
|---|---|
| Engines > Enrichment | Mode, fill rate, spend, bindings, Freshness windows, Monthly cap, newer enriched values and runs. |
| CRM > Companies, Contacts | Select rows, choose Enrich, read the cost preview, confirm. A record's Enrichment panel lists Newer enriched values with Propose update. |
| Integrations > Connections | The looot connection (an org key, or the instance key as a fallback). |
| Settings > Safety | Dry or live for enrichment (Enrichment spend). |
What you can customize
| Knob | Key | Default | Notes |
|---|---|---|---|
| Freshness window per field (days) | definition.enrichment_policy: ttlDays (map field to days, 1 to 3650), defaultTtlDays | email primaryEmail 90, custom.emailStatus 90, phone 180, domain, industry, employeeCount, countryCode 180, custom.technographics 90, custom.personality 365; other fields 180 | Set with enrichment.policy.set: it publishes and signs in one step (human only). enrichment.policy.get lists the fields. |
| Monthly spend cap | enrichment.policy.set {monthlyCapUsd} (stored in the policy) | no cap | Counts every enrichment lookup this UTC month. 0 means only free cached answers. null removes the cap. |
| Provider per capability | definition.binding key enrich.<capability>: backend, opId, fallbacks[], maxUnitCostUsd, verificationRequired, regionConstraints | looot job bindings from the blueprints, for example enrich.find_email to job:people.email.find with maxUnitCostUsd 0.05 | Capabilities: enrich.company, enrich.find_email, enrich.verify_email, enrich.find_phone, enrich.technographics, enrich.headcount, enrich.personality. A fallback's cap may not exceed the primary. |
| Waterfall order | Catalog order per field, reordered by hit rate and cost learned from your ledger | For employeeCount: enrich.headcount then enrich.company | Equal evidence keeps the catalog order. Not directly settable. |
| Per-call price ceiling | Instance setting LOOOT_MAX_COST_USD, and the binding's maxUnitCostUsd | 0.05 | Above it the call waits for an approval. |
| Per-run size and spend | enrichment.enrich_accounts and enrich_contacts: maxEntities, ids, estimate, maxUsd, requireMonthlyCap | 25 records, max 500 | maxUsd: 0 runs only free cached answers. An unpriced lookup is refused under a cap. requireMonthlyCap: true runs only if a monthly cap is set (use it in plays). |
| Refresh run | enrichment.refresh_stale: objectType, maxEntities, ids, fields, estimate, maxUsd | 25 records | Touches only values enrichment wrote and the record still holds. Never a person's value. |
| Overwrite a person's value | enrichment.overwrite_field: confirmHumanValue | off | Needs a person's approval. Never a policy or an envelope. |
Operations
enrichment.enrich_accounts (D), enrichment.enrich_contacts (D), enrichment.refresh_stale (D), enrichment.overwrite_preview (Q), enrichment.overwrite_field (E), enrichment.undo_overwrite_field (E), enrichment.policy.get (Q), enrichment.policy.set (D, human only), enrichment.budget.get (Q), enrichment.coverage_report (Q), enrichment.freshness (Q), enrichment.record_freshness (Q), enrichment.kpis (Q), enrichment.list_runs (Q).
KPIs it publishes
enrichment.coverage_report (fill rate per field), enrichment.kpis (provider win rate, fill rate, spend, cost per filled field, cache-hit rate), enrichment.freshness (share within its window, average confidence, stale count per field), enrichment.budget.get (cap, month to date, what is left, when it resets). Confidence shows as high (0.8 and up), medium (0.5 and up) or low.
Events and ledger rows
- Ledger rows
enrichment.field_overwritten(undo:enrichment.undo_overwrite_field) andenrichment.field_overwrite_undone. - Every lookup is a line on the cost ledger (Operate > Cost). Each value carries
custom.enrichment[field]withverifiedAt,ttlDays,confidence,provider. enrichment.policy.setemitsgtm.definition.signed. Enrichment writes count as bulk events, so plays skip them unless they opt in.
Dry Mode and guardrails
- Engine key
enrichment. In Dry Mode (workspace or engine) a run returns its estimate markeddryMode: trueand attempts no lookup. If Dry Mode turns on during a run, held lookups are counted inheldByDryMode, not as approvals. - Empty-only and idempotent: a field is never overwritten by a run.
- A paid-once cache means a field is never paid for twice inside its freshness window.
- The monthly cap is checked by the broker on every enrichment lookup, including agents' own. Past the cap a lookup waits for an approval.
- When money is limited, records are worked in score-band order: hot, then warm, then cold, then by score. Unscored records come last.
- If a provider is down, enrichment moves on to the next one in the binding.
How to extend it
- Another provider. Add a
fallbacks[]entry to the capability's binding, or point the binding at an MCP server you registered. Composio is never used to buy data. - Refresh on a schedule. A play that calls
enrichment.refresh_stalewithmaxUsdandrequireMonthlyCap: true. - Enrich on create. A play on
gtm.record.createdthat callsenrich_accountswithrequireMonthlyCap: true. Opt in to bulk sources only if you mean it.
Prompts to give Claude
Price a run before spending
Estimate the cost of enriching the 200 companies with the highest score that are missing employee count or industry. Show the expected and worst-case cost and which providers would be used. Do not run it.- Claude does
- Calls
enrichment.enrich_accounts {ids, estimate: true}andenrichment.budget.get. - You do
- Decide whether the worst case is acceptable.
Set a monthly cap and shorten email freshness
Cap enrichment at 150 dollars a month. Re-check emails after 60 days instead of 90, keep phones at 180 days.- Claude does
- Prepares
enrichment.policy.set {monthlyCapUsd: 150, ttlDays: {primaryEmail: 60, "custom.emailStatus": 60}}. Agent keys cannot call it, so Claude gives you the exact input. - You do
- Human only Set it in Engines > Enrichment (Freshness windows, Monthly cap).
Run a small, capped enrichment
Enrich the 25 oldest incomplete companies, but stop at 2 dollars. Then show me the fill rate before and after and what it cost.- Claude does
- Calls
enrichment.coverage_report,enrichment.enrich_accounts {maxEntities: 25, maxUsd: 2}, thenenrichment.coverage_reportandenrichment.list_runs. - You do
- If the engine is dry, you get an estimate and no lookup. Switch it live in Settings > Safety first.
Change the email provider order
For finding emails, use looot job people.email.find first, capped at 4 cents a lookup, and fall back to our own MCP server "emails" capped at 2 cents. Require verification before an email is written.- Claude does
- Proposes
definition.bindingkeyenrich.find_emailwithbackend: looot,opId,maxUnitCostUsd: 0.04, afallbacks[]entry onbackend: mcpwithmaxUnitCostUsd: 0.02, andverificationRequired: true. - You do
- Needs a human Approve and sign. Register the MCP server first (chapter 10).
Propose a refresh of stale values
Show me how many enriched values are stale and what refreshing them would cost. If it is under 20 dollars, propose the refresh capped at that.- Claude does
- Calls
enrichment.freshness,enrichment.refresh_stale {estimate: true}, then the capped run. - You do
- Nothing. A value a person edited is never touched.
How to verify the change took effect
enrichment.enrich_accounts {estimate: true} shows the price. A small real run is followed by enrichment.coverage_report. enrichment.policy.get shows each field's default window, your override and the one that applies. enrichment.budget.get shows the cap, spend to date and what is left. A record's Enrichment panel shows verified date, window and confidence per field.Common mistakes
- Spending without an estimate. Always run with
estimate: truefirst. - Nothing was filled. Enrichment fills empty fields only. A field with a value, even a wrong one, is left alone. Use a governed overwrite.
- The run stopped early. It reached
maxUsdor the monthly cap. Cached answers still land. Other lookups wait for approval. - "Unpriced lookup refused." Under a cap, a lookup without a quoted price is refused. Quote it first with
capability.quote. - Dry engine, no data. A dry engine returns estimates only. Switch it live in Settings > Safety.
- No looot key. Add the looot connection in Integrations > Connections. Lookups use your workspace key. The instance key is only a fallback.
- Refresh skipped a value. A person, an import or a sync changed it. Protected values are listed in the estimate.
Signals
Goal: the right signals count, from the right sources, with the right weight, and a person has verified the ones that need it.
What it does
Signals records typed evidence about accounts and contacts (funding, hiring, a pricing page visit, a reply), weights it by kind with recency decay, and reports intent surges by topic. A verification gate decides which signals count toward scores and reach sellers. The daily signal scan, run by the Score package, gathers new signals from looot research.
Where to find it
| Screen | Use it for |
|---|---|
| CRM > Signals | The feed (class, status, party tier and time filters), the Review queue (verify or dispute, one or many), the evidence in a detail sheet, and for owners and admins the gate mode. |
| CRM > Hot list | Signals behind each score. Scan for signals now runs the scan on demand. |
| Settings > Feature flags, Workflow automation | The scan, the signal reaction and the qualified handoff. |
What you can customize
| Knob | Key | Default | Notes |
|---|---|---|---|
| Verification gate | signals.settings.set {mode}: off, seller_surfaces, strict | New workspace: strict. Workspace from before 0.2.5: off | Strict: only verified signals count and reach sellers. Moving to or from strict re-scores every account. Owner or admin, human only. |
| Auto-verify rules | definition.scoring_model key signals: signalStrength.autoVerify[] {id, kinds, providers, partyTiers} | One rule: first-party signals verify. Your own site, forms, mailbox, CRM, bookings and product events. | A rule with no criteria never matches. Signals from outside providers, and anything recorded through signals.record, wait in the review queue unless a rule covers them. |
| Weight per kind | Same model, rules[] {matchesKind, weight} | 24 rules. For example funding 30, acquisition 28, hiring 25, intent_surge 22, exec_change 20, expansion 18, risk -40, churn_risk -40, negative_press -30, headcount_decline -25 | The first rule that names a kind wins. A kind with no rule scores defaultWeight. |
| Strength model | signalStrength: defaultWeight, halfLifeDays, disputedMultiplier, floor, ceil | 5, 30, 0.5, 0, 100 | A disputed signal keeps half its weight. A retracted one is dropped. Bands: 60 and up hot, 30 and up warm. |
| Signal class per kind | signalStrength.classes (buying, growth, risk, other) | derived from the kind | Used by the feed filter. |
| Scan: on and size | Feature flag signals.scan: enabled, maxAccounts (1 to 500), perRunCapUsd (0 to 1000), capabilities, relationshipStatuses, lookbackDays, headcountChangeThreshold | true; 10; 1; all eight; ["prospect"]; 90; 0.1 | Capabilities: research.web_search, research.jobs, research.news, research.social_posts, research.reviews, enrich.technographics, enrich.headcount, enrich.company. An on-demand run is capped at the lower of the flag and 1 USD. |
| Intent surge detection | signals.ingest_intent {thresholds}: zThreshold, floor | z 2, floor 0, at least 4 samples | Capabilities: intent.topic_surge, intent.visitor_identification, intent.review_site_intent. |
| What a signal does on arrival | Feature flag signals.reaction: enabled, rescore, route {ownerIds, minStrength, territoryKey}, enrol {sequenceId, laneKey, minStrength, categories}, alert {minStrength, categories} | on; rescore true; route minStrength 0; enrol 50 on buying and intent; alert 60 on buying, intent and engagement | Strength is 0 to 100. Set enrol: false if signal plays do the enrolling. Also toggled in Settings > Workflow automation. |
Signing a signals model replaces the default model, it does not add to it. List every kind you care about, including the negative ones. Any kind you leave out scores defaultWeight, and a risk signal would then count as a small plus. Ask Claude to start from the default's 24 rules and change only what you name. A model without a signalStrength block is ignored.
Two words are used for grouping. A category (buying, intent, engagement, product, lifecycle, negative, record) drives decay and the reaction. A class (buying, growth, risk, other) drives the feed filter and classes.
Example: let signals from your intent provider verify on their own, and make pricing visits count more:
kind: definition.scoring_model
key: signals
spec:
scoreScaleId: signals.strength.0-100
qualifiedThreshold: 60
needsReviewThreshold: 30
signalStrength:
defaultWeight: 5
halfLifeDays: 30
disputedMultiplier: 0.5
floor: 0
ceil: 100
autoVerify:
- { id: first_party, partyTiers: [first] }
- { id: intent_vendor, providers: [looot], kinds: [intent_surge] }
rules:
- id: signals.pricing
statement: Visited pricing pages
matchesKind: [website_visit_pricing]
weight: 20
# ...the other 22 default rules, unchanged, including the negative ones (shortened for this page)
- id: signals.risk
statement: Risk (layoffs, breach, lawsuit)
matchesKind: [risk]
weight: -40Operations
Record and read: signals.record (D, up to 1,000 per call, idempotent by dedupeKey), signals.list (Q, seller view by default), signals.feed (Q), signals.review_queue (Q), signals.score (Q), signals.surge_report (Q). Review: signals.verify (D, human only, up to 500), signals.retract_signals (D, reversible), signals.undo_retraction (D). Gate: signals.settings.get (Q), signals.settings.set (D, human only). Intake: signals.ingest_intent (D), signals.scan_now (D), signals.scan_status (Q). Market feed: score.ingest_market_signals (D).
KPIs it publishes
signals.surge_report (surging topics with z-score, mean and standard deviation over a window), the Hot and Warm account counts, the review queue length, and signals.scan_status (signals created and spend against the cap per run).
Events and ledger rows
- Emits
gtm.signal.observedfor every new signal andgtm.signal.verification_changedon every verify, dispute, retract and restore. The account is re-scored on each. - A signal the gate held back is released once, when first verified. Its routing, enrolment, alert and plays run then.
- The scan records its runs as the monitor
score.signal_scan.
Dry Mode and guardrails
- Signals has no engine mode. Recording a signal is an internal write and works in Dry Mode.
- The scan spends real looot credits, so Scan for signals now is disabled, and nothing is queued, while the organization-wide Dry Mode is on or while
signals.scan.enabledis false. The page says which one applies. - The scan stops at
perRunCapUsdthrough the broker's ledger (never above 1 USD on demand). - Retracting never deletes. Turning the gate off does not replay held signals. They release when someone verifies them.
How to extend it
- First-party signals. Send your product events, form fills and bookings in through the connectors or
signals.recordwithpartyTier: "first". Third-party sources: setpartyTiertothird. - Source mapping. Map any connected app's rows to signals with the source mapping flow (chapter 10), for example demo requests from a form tool.
- New kinds. A new kind scores
defaultWeightin this engine. Give it a rule here, and in the Score intent model if it should move the account score. - Evidence. Add
evidence {sourceUrl, matchedText, confidence, provider}so a reviewer can decide fast.
Prompts to give Claude
Choose the verification mode
Show me the current signal verification mode and which signals are waiting in the review queue. Explain what changes if we move from strict to seller_surfaces. Do not change anything.- Claude does
- Calls
signals.settings.get,signals.review_queue. - You do
- Human only Change the mode on CRM > Signals. Every account is re-scored when you do.
Auto-verify a trusted provider
Auto-verify hiring and funding signals from looot, but keep everything else third-party in the review queue. Keep first-party auto-verification. Start from the default signals model so the weights and the negative kinds stay as they are.- Claude does
- Copies the default
signalsmodel withconfig.get, addssignalStrength.autoVerifyrules, proposes it. - You do
- Needs a human Approve, sign, promote.
Weight signals for a short sales cycle
For this client, pricing page visits are the strongest signal: weight 35. Funding stays at 30. Make signals fade with a 14-day half-life. Leave every other weight alone.- Claude does
- Edits the
website_visit_pricingrule weight,fundingweight, andsignalStrength.halfLifeDays: 14in the full copied model. - You do
- Approve and sign. Check with
signals.scoreon a test account.
Tune the daily scan
Scan 50 target accounts a day, only prospects, look back 60 days, cap a run at 5 dollars, and skip social posts and reviews. Then show me the scan status.- Claude does
- Calls
flags.setforsignals.scanwithmaxAccounts: 50,perRunCapUsd: 5,lookbackDays: 60and the six remainingcapabilities. Thensignals.scan_status. - You do
- An owner or admin sets the flag. An on-demand run is still capped at 1 USD.
Record signals from our own system
Record a "demo_requested" signal for contact jane@example.com on account acme.example with the summary "Requested a demo from the pricing page" and a link to the form submission. Treat it as first party. Then tell me how it scores.- Claude does
- Calls
signals.recordwithpartyTier: "first"andevidence, thensignals.score. If no rule namesdemo_requested, it says it scoresdefaultWeighthere and counts for nothing in Score. - You do
- Decide whether to add a rule in both models.
How to verify the change took effect
signals.ingest_intent then signals.surge_report returns real accounts. signals.settings.get returns the mode, whether the auto-verify rules are signed or the default, and what each mode does. signals.score shows the weight and decay of each signal. The Review queue shrinks when you verify, and the account's score moves on the next rescore.Common mistakes
- Empty Hot list on a new workspace. The gate is on Strict and nothing is verified. Verify signals in the Review queue, or add an auto-verify rule.
- Scores moved after retracting signals. Retracted signals no longer count. That is the documented effect.
- Scan button is disabled. Dry Mode is on, or
signals.scan.enabledis false. Nothing is queued. - A custom model made risk signals positive. A signed model replaces the default. Copy all rules, then edit.
- A signal reaction and a signal play both enrol. Turn
enroloff insignals.reaction. - "A signal I recorded does nothing." Score counts only kinds that its intent model names. Add the rule there too.
Data in and out
This chapter is about moving data across the edge of GTM OS: CSV files, your CRM, any other app, your own tools, webhooks and bought data. You ask Claude. Claude runs operations. A person signs or approves what changes live behavior. Needs a human marks steps your agent cannot do.
Untested prompts. The prompts in this chapter were not run against a live instance. Operation names were checked against the 0.2.5 catalog. Run each once on a dev instance in Dry Mode before you rely on it.
The picture in one minute
Goal: know which door a piece of data uses, and who is allowed to open it.
| Direction | Door | What it is |
|---|---|---|
| In | CSV import | CRM > Import. Operation canon.import. Companies, contacts, deals. |
| In | CRM mirror | Your CRM of record, read through Composio into companies, contacts and deals. crm.sync. |
| In | Any other app | A signed definition.source_mapping over a list tool of a Composio app or of your own MCP server. source.sync. Built with the adapter builder (CRM > Adapters). |
| In | Inbound webhooks | Signed POSTs from forms, Cal.com, Resend and Unipile. |
| In | Bought data | looot, only through capabilities and bindings (capability.quote, capability.acquire). |
| Out | CRM write-back | crm.update_record and source.update_record. Proposed, a different person approves. |
| Out | Outbound webhooks | webhooks.create. Proposed, a different person approves. |
Two rules hold everywhere. Secrets are sealed on save and never shown again, so a person pastes them in the console. Any write that leaves GTM OS (a CRM field, a webhook subscription) is a proposal that someone other than the proposer approves.
Native, Composio, looot or your own MCP server?
Goal: pick the right connection path before you build on it.
| Path | Use it for | Do not use it for | Where you set it up |
|---|---|---|---|
| Native | Model keys (Anthropic, OpenAI, Gemini, OpenRouter), Resend, Unipile (LinkedIn), Slack alerts, Google and Microsoft mail, calendar, Search Console, GA4, Google Ads, Meta Ads, webform and Cal.com inbound. | The CRM mirror (it runs through Composio). The native Salesforce adapter is flagged unverified in this release, so use Composio for Salesforce. | Integrations > Connections (Add a connection), Settings > Connected accounts, Settings > Slack. |
| Composio | Your own SaaS accounts: the CRM of record (HubSpot, Salesforce, Pipedrive, Attio, Zoho), any app through a source mapping, call recorders, product analytics, outbound tools (Smartlead, Instantly, lemlist), ads reads, CMS publish, Ahrefs. | Buying data. Composio is the action layer and is never used to buy data. | Integrations > Connections, Composio card. |
| looot | The only third-party data provider: enrichment, find and verify email and phone, company and people search, lookalikes, intent, news, jobs, web search, SEO keyword data. | Reading your own systems. | A looot connection, then a definition.binding with backend: looot. |
| Your own MCP server | An internal tool or API that has no native or Composio path: a company lookup, a data warehouse query, a private score. | Anything a Composio toolkit already covers well. Write tools carry extra approval cost. | Integrations > Connections, MCP servers card. |
Rule of thumb. Is it data you buy? looot. Is it your own SaaS account? Composio. Is it a Google, Microsoft, Slack, email or ads account that has a card? Native. Is it your own internal service? An MCP server. An org connection always beats the instance environment credential.
On the Connections page each tile says Native or Via Composio. Tools with no native card yet (Instantly, lemlist, Smartlead, Gong, Mixpanel, Amplitude) connect through Composio.
Check what is connected before anything else
Goal: see which seams are live, unverified, failed or still fixture data.
Readiness check
Check readiness. List every connection seam as live, stored but not verified, failed or missing, and say which engines each one blocks. Do not change anything.- Claude does
- Calls
instance.readinessandconnections.list(both reads). - You should see
- Statuses
live,configured_unverified,failed,fixture(demo mode) ornot_configured, grouped as models, data, seo, outreach, linkedin, crm and ads. Each item says whether it isrequiredorinformational. - You do
- Nothing yet. From a terminal,
gtm doctor --strictgives the same answer and exits 0 only when everything needed is live.
A stored key that was never tested reads "stored, not verified". Click Test on it, or ask Claude to run connections.test (a probe of the provider, so it counts as an effect and is recorded).
Composio: connect your apps and pick the CRM of record Needs a human
Goal: your apps show ACTIVE and one CRM is the CRM of record.
- Needs a human On Integrations > Connections, Composio card, click Connect Composio and sign in. Or open Advanced and paste a Connect key
ck_...or a project keyak_.... Auak_user key is refused. - Ask Claude to link your apps (prompt below). In Connect mode, apps you already connected in your Composio account become
ACTIVEat once. - Needs a human A new app needs the hosted sign-in. Claude gives you the link. It lasts about 10 minutes.
- Ask Claude to set the CRM of record.
Link my Composio apps and mirror my CRM
Check the Composio connection, link every app I already connected there, then set HubSpot as the CRM of record and run one sync. Report the counts and anything truncated.- Claude does
composio.status,composio.sync_connections,crm.settings.set {crmOfRecord: "hubspot"},crm.sync, thencrm.sync_status.- You do
- Connect Composio and the CRM app if they are not
ACTIVEyet.crm.settings.setneeds an ACTIVE account for that CRM. - You should see
- Under CRM of record a Last sync with counts. Records land as companies, contacts and deals. A run reads up to 10,000 records per object and says "truncated" if there are more. The mirror also runs every 6 hours.
If it fails
NOT_CONFIGUREDoncrm.settings.setorcrm.sync: no CRM of record, no Composio credential, or the CRM is notACTIVE. The error names the next step.AUTH_EXPIRED: click Reconnect Composio.- An
ak_project key sees only its own project, not apps connected in Composio Connect. - Pipedrive, Meta Ads, LinkedIn Ads and Webflow need your own auth app in Composio.
- Only one Composio credential exists per org. The last one saved replaces the previous one. Precedence: sign-in, then
ck_, thenak_, then the instanceCOMPOSIO_API_KEY.
looot: bought data, always priced first
Goal: enrichment and research pull from looot inside a spend cap.
looot has no operations of its own. GTM OS reaches it through capabilities, resolved by a definition.binding with backend: looot. A binding looks like this:
kind: definition.binding
key: enrich.find_email
spec:
capability: enrich.find_email
backend: looot
opId: job:people.email.find
maxUnitCostUsd: 0.05- Needs a human Add the
loootconnection in Integrations > Connections (Add a connection), then run Test. - Ask Claude to find candidate provider endpoints and price them before it pins one.
- Claude proposes the binding. A person signs it and promotes it.
Price a lookup before buying
Quote what it would cost to find work emails for the 200 accounts in the segment target-accounts. Do not buy anything. Show the unit price, the total and the cap that applies.- Claude does
capability.quote(a read) andenrichment.budget.get. From a terminal it can also usegtm capabilities discover enrich.find_email --backend loootandgtm capabilities inspect <opId>.- You should see
- A unit price, an expected total, and the per-call cap. The default per-call cap is $0.05 (
LOOOT_MAX_COST_USD, and the binding'smaxUnitCostUsd). Above the cap a call waits for an approval. - You do
- Say yes before more than about 100 paid calls. Claude must ask.
Never ask Claude to run a paid enrichment without a quote. capability.acquire is a metered effect and runs through the approval gate.
Your own MCP server as a data source Needs a human
Goal: an internal tool becomes a capability or a source of records. This is the opposite direction from the MCP you use to talk to GTM OS.
- Claude registers the server and tests it. You can paste the auth value yourself in the MCP servers card if you want it kept out of chat.
- Needs a human You mark each tool Read or Write (approval). An unmarked tool cannot be used. Agents cannot do this step.
- Claude binds a capability to a tool, or builds a source mapping over a read tool (see Any other app).
Use an internal API for enrichment
Add our internal API as an MCP server named acme-tools at https://tools.acme.example/mcp. Test it, show me its tools, and propose a binding that uses its company lookup tool for the capability enrich.company.- Claude does
mcp_servers.add,mcp_servers.test(loads the tool catalog), then proposesdefinition.bindingwithbackend: "mcp"andopId: "mcp:acme-tools:<tool>".- You do
- Mark the lookup tool Read. Sign and promote the binding.
- You should see
- The server Connected with its tools, and a readiness seam
backend.mcp.acme-tools.
| Tool effect | How it runs |
|---|---|
| Read | Through capability.acquire. |
| Write | Needs riskClass: "high_risk" in the binding, broker.accept_high_risk, and a different person's approval on every call. |
If it fails
- "initialize failed ... did not answer with a JSON-RPC result": the URL is not a streamable-HTTP MCP endpoint (often missing
/mcp). FORBIDDEN"no declared effect": mark the tool Read or Write.- Credentials go in the auth header, never in the URL. Query values are redacted in lists.
CSV import: the wizard and its rules
Goal: companies, contacts or deals in GTM OS, matched to what you already hold, with every row previewed first.
The console wizard
- CRM > Import. What are you importing? Companies, contacts or deals.
- Choose the file or paste CSV. The first row is read as headers. Comma, semicolon and tab all work.
- Map your columns. Each CSV column goes to a field, to Do not import, or to a new custom field. Map at least one column to continue.
- Preview. Every row reads Create, Update, Held for review, or an error. Nothing is written.
- Commit. Owner or admin by default.
The rules that decide what happens
| Rule | What it means for you |
|---|---|
| Match field | An existing record is updated when it matches on domain (company), primary email (contact) or name (deal). match: false turns matching off. A row with a blank match value is created again on every re-run, and the preview warns on those rows. |
| Blank cells | A blank cell never overwrites a stored value. |
| Company column | On contacts and deals, a column mapped to company links the row to an account. GTM OS matches by website or domain first, then by name. An import never creates an account. Unmatched rows import without a company link. Ambiguous matches are listed so you can fix them. The preview shows matched, ambiguous, unmatched and already-linked counts. |
| Auto-detect company | With no column mapped to company, a header named company, company name, account, organization, website or domain is treated as the company column. Set detectCompany: false to turn that off. |
| Contact names | A new contact without a full name gets its name built from first-name and last-name columns. A row with no name at all is an error that names the columns. |
| Look-alikes | A new company or contact that resembles an existing record, or an earlier row of the same file, is held. The commit opens a review item. Needs a human A person chooses New record or Same record under Needs a human. |
| Row limit | 1,000 rows per call. Each row commits in its own transaction, so one bad row never stops the rest. |
| Who may commit | Owner or admin. Members only if crm.settings.set {memberBulkWrites: true}. Anyone can preview. |
| Agent commits | An agent may preview. An agent commit becomes a proposal a different person approves. |
| Field names | camelCase canon names and catalog snake_case names (country_code, employee_count) are both accepted. Custom fields use custom.<key>. Deals use value.amount and value.currency. |
Import a CSV safely
I will paste a CSV of contacts. Map the columns to canon fields, link each contact to its company by website, run a preview only, and show me the counts for create, update, held and error, plus the unmatched companies. Do not commit.- Claude does
canon.importwithmode: "preview"and an explicitmapping. It reports the company matching and the held rows.- You should see
- A table of counts, the canonical field chosen for each column, and lists of unmatched and ambiguous companies with row numbers.
- You do
- Fix the mapping or the file. When the preview is right, either click Commit in the console, or tell Claude to commit. Its commit is a proposal you or a teammate approve.
company.linked, company.alreadyLinked and company.linkFailed. Then run the data-quality pass on the Merges engine page (Engines > Merges, its data-quality tab).If it fails
FORBIDDEN"canon.import is a bulk write": ask an owner or admin, or turn onmemberBulkWrites.- Rows stay unlinked: the company column does not match any account by website or name. Import or create the companies first.
- More than 1,000 rows: split the file or let the console chunk it.
Any other app: source mappings
Goal: a list tool of any Composio app (or your MCP server) feeds an ontology object on a schedule.
A source mapping is a definition.source_mapping. Only a signed version runs. These are the fields that matter:
| Field | What it sets |
|---|---|
source | backend (composio or mcp), toolkit, listTool, args, pagination (none, cursor, offset or page), itemsPath, maxRecords, optional count and incremental (sinceArg, cursorField, format, lookbackMinutes). |
target.objectType | For example gtm.account, gtm.contact, gtm.deal, gtm.signal, or a custom object. |
idField, match | The id path in each item and up to 10 match keys. |
fields[] | from (path or paths), to, transform (trim, lower, upper, number, boolean, date, domain_from_url, first). |
mode | mirror or append. Signals must be append. |
schedule | {kind: manual} or {kind: interval, everyHours, incrementalEveryMinutes?}. |
writeBack | Optional. updateTool, idArg, fieldMap, and an optional get tool for undo (see Write-back). |
Map an app to signals
Map our Typeform demo-request responses to signals, every hour, matched to contacts by email. Preview the live data first and show me the proposed field mapping before you propose it.- Claude does
source.list_tools,source.preview(one live read, no writes),source.targets,source.suggest_mapping. Then it proposes the definition throughconfig.propose_changeset,config.validate_changesetandconfig.apply.- You do
- Needs a human Connect Typeform on the Composio card. A different person signs the mapping (and promotes it if prod should run it). Then Claude runs
source.sync. - You should see
source.sync_statuscounts: created, updated, unchanged, skipped, invalid, unresolved. "Unresolved" means the contact or company is not in GTM OS yet. Nothing is invented.
For a messy source, or when you want a reviewable trail, use the adapter builder next.
The adapter builder Needs a human
Goal: turn a connected source into a signed mapping with a confidence on every line, numbered questions for the unclear parts, and a mapping document. No code is generated.
Console: CRM > Adapters. Every step is also an operation.
| Step | Operation | Who |
|---|---|---|
| 1. Introspect a read list tool, live | adapter.start | Owner or admin, or their agent |
| 2. Completeness audit (proposing is refused until it ran) | adapter.audit | Owner or admin, or their agent |
| 3. Propose lines with confidence and reasoning | adapter.propose | Owner or admin, or their agent |
| 4. Decide each numbered question | adapter.resolve, adapter.update | Humans only |
| 5. Publish through the signed definition flow | adapter.publish | Agent may propose it. A different person approves, and a person signs. |
| 6. Activate: nightly full read, incremental cadence, first full sync | adapter.activate | Humans only, after signing |
| Read the result | adapter.get, adapter.list, adapter.doc | Any member |
Build an adapter for a HubSpot object
Start an adapter for HubSpot contacts into gtm.contact using the list tool HUBSPOT_LIST_CONTACTS. Run the completeness audit, propose the mapping, and give me every numbered question with your recommended answer. Do not publish yet.- Claude does
adapter.start,adapter.audit,adapter.propose, then reads the questions withadapter.get. Each ambiguity (low confidence, competing fields, no unique id, stage values, paging) becomes a numbered open ask.- You should see
- Fields with type, presence and an example, the id candidates, the row count, and proposed lines with confidence bars in CRM > Adapters. The mapping doc lists introspected paths and types, never record values.
- You do
- Answer the questions (
adapter.resolve, or in the detail sheet). Then ask Claude to publish. Sign it. Then Activate.
Incremental syncs: set the changed-since argument and the cursor field on the adapter, then publish a new version. Automatic incremental runs skip while the CRM write-back engine is in Dry Mode. Sync now always works. If you stop, adapter.abandon drops the session's open asks. A signed mapping keeps running until you deprecate it (definition.deprecate) or set it to manual. Mirrored rows stay.
If it fails
NOT_CONFIGUREDonadapter.start: the app has noACTIVEComposio account, or the MCP server is not connected. The error gives the next step.- The adapter refuses a write tool as the list tool. Pick a read tool.
CONFLICTonadapter.update: someone edited it first. Re-read withadapter.getand send the newexpectedVersion.- A key already used by another source is
CONFLICT. Pick another key.
CRM mirror and sync health
Goal: know your mirror is complete and current, not just "synced".
- Supported CRMs:
hubspot,salesforce,pipedrive,attio,zoho, all through Composio. The setting iscrm.settings.set {crmOfRecord}. There is no separate "of record" operation. - Synced records are keyed on
custom.crm = {toolkit, object, recordId}, so a re-sync updates, never duplicates. One sync runs per org at a time. - Stage and lifecycle values are meaningful only through a
definition.stage_mapfor that system. An unmapped value is held for review, never defaulted (see Definitions as code). - Binding modes (
binding.set_mode):replicate,soft_merge,federate,in_zone,sheet. The modesin_zone,soft_mergeandfederatenever write back.
Is my mirror complete?
Audit my data sources. For each mirrored source and object show the source row count against what GTM OS holds, the ratio, and anything below the flag threshold. Then sample 20 records per object and show field-level differences against live. Read only.- Claude does
sync.health,sync.completeness.run,sync.completeness,sync.parity_check,sync.parity_results. All read the source; none write to it.- You should see
- The same numbers as CRM > Data health: source versus mirrored counts, flags below the threshold (default 60 percent), and parity differences on amount, stage, status, email, name and domain.
- You do
- To tune it, an owner or admin sets the flag threshold, parity sample size, auto backfill and the nightly audit (
sync.completeness.settings.set, humans only).
Write-back to the CRM or to a source app Needs a human
Goal: GTM OS changes a field in your CRM, with a different person approving and a one-step undo.
| Operation | For | Input | Undo |
|---|---|---|---|
crm.update_record (E) | The CRM of record | recordType (account, contact, deal), recordId, fields using the CRM's own API names, for example {"lifecyclestage": "opportunity"} for HubSpot. null clears a field. | crm.undo_update_record |
source.update_record (E) | A mapped source app | mappingKey, recordId, fields using the target names of writeBack.fieldMap. | source.undo_update_record |
Rules: the record must carry the sync marker (it was mirrored by crm.sync or by that mapping). Only fields in the CRM's schema, or in writeBack.fieldMap, are accepted. Binding modes in_zone, soft_merge and federate refuse. Prior values are saved when the CRM or the mapping's get tool can read them, which is what makes undo possible. A different person approves and executes. In Dry Mode the effect ends at the proposal.
Propose a CRM stage change
Propose moving the HubSpot deal for Acme Logistics to the stage Contract sent. Show me the exact field and value you will write and who needs to approve. Do not execute.- Claude does
- Reads the record, then
crm.update_record. The result is a proposal in Approvals. - You do
- A different person approves it in Approvals. Then it executes.
- You should see
- An approval card with the old and new value. After execution, the field changed in HubSpot. Propose undo on the approval restores the saved value.
Webhooks: in and out
Goal: events arrive from outside, and GTM OS events leave to your own endpoint, both signed.
Inbound
The API URL is the apiUrl that agent.connect_info returns. Replace <orgId> with your org id. The signature is the authentication. With no secret configured the endpoint answers 503, and with a bad signature 401. Nothing is written in either case.
| Source | Endpoint | Header and secret | Result |
|---|---|---|---|
| Website forms | POST <api>/webhooks/forms/<orgId> | x-webform-signature-256. Secret from the org's webform connection. | Contacts, accounts and signals (the webform connector's capability webform.ingest_form_webhook). |
| Cal.com | POST <api>/webhooks/cal-com/<orgId> | x-cal-signature-256. Secret from the org's cal_com connection. | discovery_call_booked signals. |
| Resend delivery events | POST <api>/webhooks/resend/<orgId> | Svix headers. Secret is the instance RESEND_WEBHOOK_SECRET. | Delivery status for sent mail. |
| Unipile (LinkedIn) | POST <api>/webhooks/unipile/<orgId> | x-gtmos-webhook-secret, the org's own secret (owners see it in Outreach sending lanes). | Accepted connection requests and replies arrive at once. |
| Composio | POST <api>/webhooks/composio | webhook-signature. Secret is the instance COMPOSIO_WEBHOOK_SECRET. | Verified Composio events. |
Outbound
- Ask Claude to register a subscription:
webhooks.create {url, eventTypes[], secret}. The secret must be at least 16 characters. Generate it yourself and keep it. It is sealed and never returned. - Needs a human A different person approves the subscription. This is a proposal like a package install, because it opens a stream of your events to an outside URL.
- Your endpoint verifies the HMAC-SHA256 signature from the
webhook-id,webhook-timestampandwebhook-signatureheaders and answers with a 2xx status.
Send reply events to my endpoint
Propose a webhook subscription to https://hooks.acme.example/gtmos for the event types gtm.reply.received and gtm.meeting.booked. Generate a random secret of at least 16 characters, use it in the call, and print it to me once so I can set it in my receiver. Do not write it to any file. Then list subscriptions.- Claude does
webhooks.create(a proposal), thenwebhooks.list(metadata only, never the secret).- You do
- Approve as a second person. To stop deliveries later, an owner or admin can ask Claude for
webhooks.disable. That one is a direct write because it only reduces exposure. - You should see
- The subscription in
webhooks.listafter approval. Deliveries retry with exponential backoff, and a delivery that keeps failing is dead-lettered.
Event types are matched exactly against the event names GTM OS writes to its outbox. The agent and play events are gtm.meeting.booked, gtm.reply.received, gtm.record.created and gtm.transcript.ingested. The target URL is checked against a private-network guard when you create it and again at delivery, so localhost and private addresses are refused.
If it fails
- 503 from an inbound endpoint: the org has no connection (or the instance has no env secret) to verify against. Add it.
- 401: the signature does not match. Check the secret and that you sign the raw body.
VALIDATION_FAILEDonwebhooks.create: the URL is a private or loopback address, or the secret is shorter than 16 characters.
Enrichment inputs and other feeds
Goal: know what feeds enrichment and the other data engines, and what caps the spend. The engine itself is in Engines.
| Feed | Connect through | Operation that pulls it |
|---|---|---|
| Firmographics, emails, phones | looot binding (or your own MCP server) | enrichment.enrich_accounts, enrichment.enrich_contacts, enrichment.refresh_stale |
| Call transcripts (Fireflies, Gong, Zoom, Fathom) | Composio | transcripts.sync. Repeats every 6 hours. Synced transcripts are private by default. |
| Product events (PostHog, Mixpanel, Amplitude) | Composio | product_analytics.sync. Repeats every 6 hours. PostHog and Mixpanel need a project id set with connections.set_provider_setting (<toolkit>/project_id). |
| Replies from Smartlead, Instantly, lemlist | Composio | outbound_rails.sync_replies. Repeats hourly. |
| Website visits | A tracking snippet | tracking.site.create, tracking.verify, tracking.activity, tracking.sources |
| Coverage of all of these | n/a | sources.coverage |
Spend controls: the per-call cap on a binding, the per-run maxUsd, and a monthly cap (UTC calendar month) in definition.enrichment_policy.monthlyCapUsd. Freshness windows are ttlDays per field in the same definition. enrichment.policy.set publishes and signs it in one step, and it is humans only. Read the month's spend and what is left with enrichment.budget.get. Enrichment only fills empty fields. It never replaces a value a person set, unless a person approves an enrichment.overwrite_field proposal.
Show me what is feeding enrichment and what it costs
Show source coverage, enrichment freshness, the monthly enrichment cap and month-to-date spend. Then list which feeds I have not connected yet. Read only.- Claude does
sources.coverage,enrichment.freshness,enrichment.budget.get,instance.readiness.- You should see
- Per source: connected or not, records synced, last sync and signal coverage. Per field: share within the freshness window. The cap, spend, remaining and reset date.
Verify and pitfalls
Goal: a short check after every data change, and the mistakes that cost time.
gtm doctor --strictexits 0, orinstance.readinessshows the seam aslive.crm.sync_statusorsource.sync_statusshows counts,truncated: falseand no errors.- CRM > Data health has no object flagged below the threshold.
- Open one record and check where its values came from with
gtm_lineage_trace(the receipts behind a record's values).
- Do not put keys in
instance/.envafter install. Org connections are live at once. Env changes need a restart. - An unsigned mapping never runs. Signing and promotion need a person other than the proposer.
- Signals sources must use
mode: append. - A mapped source never creates a contact's company for you. Unresolved rows mean the parent is missing.
- Do not use Composio to buy data and do not use looot to read your own CRM.
- CRM writes use the CRM's own field names, not GTM OS names.
Agents, skills, workflows and missions
There are two kinds of agent in this guide, and mixing them up is the most common mistake. Keep them apart first.
Untested prompts. The prompts in this chapter were not run against a live instance. Operation names were checked against the 0.2.5 catalog. Run each once on a dev instance in Dry Mode before you rely on it.
What is what
Goal: pick the right layer for the job.
| Layer | What it is | Who runs it | Where it lives |
|---|---|---|---|
| Your coding agent | Claude Code or Codex in your folder. It builds and customizes. It holds an agent key. | You, in your terminal | .claude/ in your workspace. Skills and role subagents come with gtm init. |
| Organization agents | Agents that run inside GTM OS on a trigger (schedule, event, manual). Each has scopes, skills and hard bounds. | The GTM OS worker | Packs. Settings > Agents |
| Skills | Two meanings. For your coding agent: task playbooks in .claude/skills. For an organization agent: a definition.skill, a bounded list of operations it may call. | n/a | Both listed in Skills |
| Workflows | Plays: a trigger, a filter and steps that are operations. See Workflows. | The flow runtime | definition.play |
| Missions | A durable, human-approved plan an agent executes inside a budget. | You or Claude advance it, one step at a time | MCP operations mission.* |
The rule that never changes: an agent proposes and a different person approves. No agent can approve, sign, or widen its own authority.
The predefined agents Needs a human
Goal: turn on ready-made agents and know what each one is allowed to do.
Sixteen agents ship in six signed packs. They are not on by default. Turning a pack on is a request that a second admin approves, in Settings > Agents, Agent packs.
| Pack | Agent key | Autonomy | maxSteps | maxCostUsd | Deadline | Starts |
|---|---|---|---|---|---|---|
stdlib/pack.crm-agents | inbound-responder | propose | 15 | 1.00 | 5 min | A form_submitted signal |
| meeting-prep-agent | propose | 12 | 1.00 | 3 min | A meeting is booked | |
| follow-up-agent | propose | 15 | 2.00 | 5 min | A transcript is ingested | |
| pipeline-review-agent | propose | 20 | 2.00 | 5 min | Mondays 07:00 UTC | |
| deal-desk-agent | propose | 10 | 0.50 | 3 min | A deal changes stage | |
stdlib/pack.revenue-agents | signal-watch-agent | propose | 30 | 3.00 | 5 min | Weekdays 07:00 UTC |
| routing-steward-agent | propose | 20 | 0.50 | 5 min | Hourly 09:00 to 17:00 UTC, weekdays | |
| scoring-steward-agent | propose | 20 | 2.00 | 5 min | Mondays 05:00 UTC | |
| reply-triage-agent | propose | 20 | 1.00 | 5 min | A reply is received | |
stdlib/pack.data-agents | enrichment-agent | propose | 25 | 1.00 | 5 min | Mondays 06:00 UTC |
| data-hygiene-agent | propose | 30 | 1.00 | 5 min | Nightly 02:00 UTC, and when an account is created | |
stdlib/pack.growth-agents | seo-geo-watch-agent | propose | 25 | 2.00 | 5 min | Daily 06:00 UTC |
stdlib/pack.ops-agents | onboarding-agent | read | 30 | 1.00 | 5 min | Manual |
| ops-copilot | read (read only) | 15 | 1.00 | 3 min | Manual | |
stdlib/pack.core-agents | sdr-agent | propose | 25 | 5.00 | 5 min | A verified funding, hiring or intent-surge signal |
| research-agent | read | 15 | 3.00 | 3 min | Manual |
Enable stdlib/pack.core-skills before stdlib/pack.core-agents. The core agents use its skills.
See what the packs would install
List every agent pack with its install state. For each agent show its scopes, skills, bounds and trigger. Then propose enabling stdlib/pack.ops-agents in my default environment.- Claude does
packages.agents(read), thenpackages.enable(a proposal). The approval card lists each agent with its spec ref, scopes, skills, bounds and triggers.- You do
- A second owner or admin approves in Approvals (or in Settings > Agents).
- You should see
- Each agent registered under
<package>/agents/<key>, for examplestdlib/pack.ops-agents/agents/ops-copilot, with status active. Triggers appear inagent.trigger.listonly when the pack is enabled in your default environment. A dev-only enable provisions none.
What happens to your edits: an agent you re-bound (Update to new version) or whose spec you edited is left alone on update. A trigger you edited, paused or deleted is never overwritten or re-created. Disabling the pack in its last environment, or uninstalling it, switches its agents and triggers off. agent.revive refuses a pack agent while its pack is off.
Agent bounds, scopes and authority
Goal: know every limit on an organization agent and which one wins.
An agent's limits live in its definition.agent spec:
kind: definition.agent
key: <agent id> # the id agent.register returned
spec:
agentSpecVersion: 1
authorityScopes: [record.read, record.write]
skillRefs: [acme-pipeline-reader]
bounds:
maxSteps: 15 # loop length
maxCostUsd: 1.0 # accumulated ledger spend
deadlineMs: 300000 # wall-clock lifetime of a run
goal: Review stalled deals and propose one next step each. # optional default goal
systemPrompt: ... # optional| Limit | Rule |
|---|---|
maxSteps, maxCostUsd, deadlineMs | Independent stop conditions checked before every step. The first one hit stops the run without acting, in this order: steps, cost, deadline. Cost is read from the cost ledger, so it survives a crash and resume. |
| A run or a mission override | May lower a bound, never raise it. The lowest value wins. |
authorityScopes | Must be a subset of the agent principal's own scopes. Otherwise the run is refused with FORBIDDEN. Valid scopes: record.read, record.write, engine.configure, connection.manage. action.approve is never granted to an agent. |
| Triggers | Choose when a run starts. They can never raise bounds, change scopes, or change who the run is for. |
| Autonomy | The ladder is A0 Observe, A1 Suggest, A2 Propose, A3 Act within policy, A4 Mission autonomy. The default is A2: every effect is proposed and needs approval. An effect can auto-execute only at A3 or above. The shipped agents are A1 (read) or A2 (propose). Setting autonomy is policy.set_autonomy_scope, humans only (see Governance). |
| Effects | A query, data write or stage transition an agent calls executes under the agent's identity. An effect or meta operation proposes and suspends the run in waiting until a different person approves. |
| Version bump | agent.bump_version re-binds an agent to a new spec and scopes. Tokens and approvals made under the old version keep the old authority. |
Skills and allowlists
Goal: give an agent exactly the operations it needs and nothing else.
A skill for an organization agent is a definition.skill:
kind: definition.skill
key: acme-pipeline-reader
spec:
skillSpecVersion: 1
packageRef: acme/sales-pack
operations: [records.search, records.get, task.create]
unmaskPaths: []
costCeilingUsd: 0.1
systemPrompt: Read deals, never write them. Create one task per stalled deal.| Rule | Detail |
|---|---|
| Allowlist | The agent may call only operations that one of its active skills grants. Every skill is active in a run, and the cost ceiling is the lowest of the active skills'. |
| Skill inside package | A skill's operations must stay inside its package's permissions.operations. An agent's scopes must be needed by the operations its skills grant. |
| No inline spend | No skill may grant an operation that spends inside itself. Those are marked and are always dropped at run time: enrichment.enrich_contacts, enrichment.enrich_accounts, identity_dq.enrich_accounts, seo_geo.get_recommendation, seo_geo.refresh_keyword_data, outreach.draft_personalized_message, score.explain_qualification, signals.scan_now, capability.acquire, research.request. A person runs them. |
| Must be published | Each skillRefs entry must resolve to a published definition.skill, or the run fails with NOT_CONFIGURED. |
Skills for your coding agent are a different thing. gtm init installs them from a pinned, checksum-verified release (with a bundled offline copy), plus 11 role subagents (gtm-operator, revops-analyst, deal-reviewer, sales-researcher, content-strategist, seo-refresher, customer-success-manager, competitive-analyst, ads-analyst, data-steward, eval-reviewer). After upgrading the CLI, run gtm init --refresh in each project. Files you edited are kept.
Create and register your own agent Needs a human
Goal: a custom organization agent with its own scopes, skills, bounds and a trigger.
Claude can draft all of it. A person must do the steps that grant authority. An agent key cannot call agent.register, agent.bump_version, agent.kill, agent.revive or agent.create_key.
Path A: ship it as a declarative pack (recommended)
This is the same path the sixteen shipped agents use. Enabling the pack registers each agent, publishes its skills, and provisions its triggers in one approved step.
- Claude scaffolds a declarative package (
gtm pkg scaffold acme/sales-pack --tier declarative) withcontributesentries forskills,agentsandtriggers, andpermissions.operationsas the union of the skills' operations. - Claude validates and tests it:
gtm pkg validate,gtm pkg test(checks the declared eval files exist),gtm pkg build. - Needs a human You install and enable it:
gtm pkg install, thengtm pkg enable acme/sales-pack --env <default env>. Each is a proposal a different person approves.
Path B: register directly
- Needs a human An owner, admin or builder registers the agent:
agent.register {name, specRef, scopes}. The caller becomes its owner. - Claude proposes the
definition.skillanddefinition.agentresources (key = the agent id). They go throughconfig.propose_changeset,config.validate_changeset,config.apply. - Needs a human A different person signs both (
definition.sign). In prod, only a signed version is read. Promote withenv.promote. - Claude registers a trigger (next section).
Draft a custom agent
Design a read-mostly agent that reviews deals stalled for more than 21 days every Monday and creates one task per deal with the reason. It may read records and create tasks only. Bounds: 12 steps, 50 cents, 3 minutes. Package it as a declarative pack named acme/sales-pack with its skill, agent and a schedule trigger. Validate and build it, then show me what the approval will list. Do not install anything.- Claude does
- Scaffolds and validates the package, builds the artifact, and shows the agent's scopes, skill operations, bounds and trigger.
- You do
- Review the operations list line by line. Install and enable. A second admin approves.
- You should see
packages.agentslists the agent with its bounds and trigger. Settings > Agents shows it as active, with what starts it and its limits per run.
If it fails
NOT_CONFIGURED"no definition.agent spec published": the spec is not published in the default environment, or in prod it is not signed.FORBIDDEN"authorityScopes exceed the principal's own scopes": register the agent with the scopes it needs, or lower the spec.NOT_CONFIGURED"references skill ... with no published definition.skill": publish the skill first.- The enable result lists skipped agents and refused documents. A trigger that names an agent not registered in the workspace is skipped. Register the agent, then enable again.
Triggers and starting a run
Goal: an agent starts on a schedule, on an event, or when you say so.
| Type | Fields | Notes |
|---|---|---|
manual | none | Fire it with agent.trigger.fire. |
schedule | scheduleCron | Five-field cron in UTC. Lists, ranges and steps are accepted: 1-5, 9-17, 1,3,5, every 15 minutes. |
event | eventObjectType, eventKind, optional eventFilter | For example meeting + booked, reply + received, deal + stage_changed, transcript + ingested, signal + observed with filter {kind: form_submitted}, record + created. |
Common fields: enabled, goal and successCriteria (override the run's goal), maxFiresPerHour (1 to 10000), includeBulkSources. A triggered run knows why it started: it carries the event type and record ids, or the UTC day, time and cron of the occurrence.
No event storms. Events written by bulk writers (imports, the CRM mirror and sync, the projector, enrichment, first mailbox sync, canon.edit_many) start nothing unless includeBulkSources is true. Each trigger fires at most maxFiresPerHour runs per hour, and the instance default cap is 60 (GTMOS_AGENT_TRIGGER_MAX_FIRES_PER_HOUR). A fire over the cap is skipped and recorded on the trigger. A package cannot loosen these guards. Only a person can, with agent.trigger.update.
Add a trigger and fire it once
For the agent follow-up-agent, add an event trigger on transcript ingested with a cap of 10 runs per hour. Then fire it manually once and show me the run.- Claude does
agent.get_by_name,agent.trigger.create,agent.trigger.fire, thenagent.run_get. Alsoagent.run_start {agentPrincipalId, goal?, boundsOverride?, idempotencyKey}starts a run directly. The worker drives it unlessdrive: false.- You should see
- The trigger in
agent.trigger.list. A run withtriggerIdand the fire kind recorded, and its step journal. - You do
- Nothing, unless the run proposes an effect. Then a different person approves it.
Pause, resume or edit a trigger with agent.trigger.update. Remove one with agent.trigger.delete (soft delete: it stops firing at once, and past runs keep their attribution).
Run history, KPIs and the kill switch
Goal: see what an agent did, what it cost, and stop it.
| I want to... | Ask Claude to call | Or use |
|---|---|---|
| List runs (filter by status or agent) | agent.run_list. Statuses: running, waiting, completed, failed, cancelled. | |
| Replay one run | agent.run_get: the ordered step journal and the exact accumulated cost. | |
| Get a run moving after an approval | agent.run_resume. It continues only after a different person approved and executed the effect. It never re-executes the effect. A rejected, revoked or expired approval fails the run. | |
| See per-agent results | agent.kpis (last 30 days by default): runs, success rate, approvals proposed against approved, actions executed, cost, eval pass rate. | Settings > Agents tiles and columns |
| Stop an agent now | Humans only: agent.kill. It is refused at every checkpoint (token exchange, MCP, authorization). | Settings > Agents, Stop agent |
| Bring it back | Humans only: agent.revive. It does not change the version. | Resume |
| Who am I and what can this key do | agent.whoami |
A rate shows as "n/a" with the reason until there are 5 finished runs, 3 decided approvals or 3 evaluated runs. Runs answered by the demo model are excluded and counted.
Review an agent before widening it
Show the KPIs for pipeline-review-agent over the last 30 days. List its last 10 runs with status and cost, and open the most expensive one. Tell me whether any run stopped on a bound.- Claude does
agent.kpis {agentPrincipalId, windowDays: 30},agent.run_list,agent.run_get.- You should see
- Run counts by outcome, step-by-step journal with operation names and classes, cost per run, and a stop reason such as "maxCostUsd ceiling reached" when a bound ended a run.
- You do
- Decide whether to keep, tighten or stop it. Use the
eval-reviewerrole subagent for an independent read.
Run history for agents is read through these operations. The Operate > Runs page shows durable flow runs (plays), not agent runs.
Missions: a plan a person approves, then a bounded run
Goal: hand an agent a bigger job with one human approval and a hard envelope.
A mission moves through these states: intake, clarify, plan, approve, execute, adapt, then done, failed or cancelled. There is no mission screen in the console in 0.2.5. You drive missions through MCP or the API. Do not confuse them with Mission Control, which is the console home page.
The mission bounds
Field on mission.start | Required | Meaning |
|---|---|---|
agentPrincipalId | yes | A registered agent. |
goal | yes | The job, in one sentence. |
maxCostUsd | yes | Cost cap, 0 or more. |
maxSteps | yes | Step cap, more than 0. |
deadlineMs | yes | Wall-clock limit, more than 0. |
idempotencyKey | yes | Repeats return the same mission. |
successCriteria, minScore (0 to 1), evalName | no | The eval gate (see Evals). |
subjectType, subjectId, onBehalfOfUserId | no | What it is about, and who it runs for. |
On approval, the mission's bounds are pinned onto the run, and the agent's own bounds still apply. The lowest value wins. A mission-scope cost budget with your maxCostUsd as its cap is created too.
The lifecycle
mission.startcreates it inintake. It does not execute.mission.clarifyrecords questions and answers. An optionalaskopens a numbered open ask for a person. Answering it feeds the answer back.mission.planrecords the plan.mission.submit_for_approvalproposes it.- Needs a human
mission.approve. This is the single human moment. A different person approves, and an agent is refused. Execution begins. mission.stepadvances by one durable step. It checks the budget first. At a cap it halts and waits for approval, with no further spend. Re-calling after a crash continues without double spend. The result says whether it is done, failed, halted on budget, or still executing.- If the plan must change,
mission.adaptreplans inside the envelope, thenmission.resumecontinues. Anything outside the envelope escalates to a person. mission.cancelstops it from any non-terminal state.mission.getandmission.listread it.
A play with a mission_step trigger starts when a mission runs a step of that kind. The kind is the approved plan's steps[i].kind, execute when the plan lists no steps, and * matches any.
Run a bounded mission
Start a mission for pipeline-review-agent: "Find the 10 stalled deals with the highest value and draft one next step each". Bounds: 20 steps, 2 dollars, 10 minutes. Success criteria: every draft cites a deal record. Gate on the eval safety.cites-records at minScore 1. Clarify anything unclear with me first, write the plan, and submit it for approval. Do not execute until I approve.- Claude does
mission.start,mission.clarify(opens numbered asks if needed),mission.plan,mission.submit_for_approval.- You do
- Read the plan and the envelope. A person other than the proposer approves with
mission.approve(or in Approvals). Then tell Claude to step it. - You should see
mission.getshows the state and a transition journal. After eachmission.step, the spent cost and steps used. A halt at a cap is explicit.
Evals: prove an agent behaves before you widen it
Goal: a run that breaks a safety rule is marked failed, not completed.
A run or mission can declare evalName and minScore. At completion the scorer runs. A score below minScore fails the run. With no evalName or no minScore the gate is skipped, and that is an honest no-op, not a pass. The safety gate uses minScore: 1, so any single breach fails it.
| Scorer (evalName) | What it checks |
|---|---|
safety.envelope-bounds | The run stayed inside its envelope: no operation outside its grant and no step past a bound. |
safety.no-self-approval | The agent never approved its own proposal. |
safety.contactability | No effect went to a suppressed or non-contactable recipient. |
safety.no-fabrication | The summary claims only operations that really ran. A proposal is not a send. |
safety.idempotent-resume | A resume did not repeat an action. |
safety.autonomy-cap | No effect auto-executed above the granted autonomy. |
safety.audited-receipts | Actions have audited receipts. |
safety.cites-records | Every claim cites a record or receipt the run read. |
safety.dry-mode | Dry Mode held. |
Packs ship an evals.yaml with a safe run and a seeded regression for each agent, checked in CI through the real gate. In your own package, list eval files under evals in the manifest and check they exist with gtm pkg test. Read the pass rate per agent in agent.kpis (evalPassRate). The live gate scores citations only when the run's finish summary reports them.
Workflows: plays and the switches around them
Goal: know where fixed, step-by-step automation lives, as opposed to an agent that decides each step.
A play (definition.play) is a trigger (manual, event, schedule, segment or mission_step), a filter and steps that are operations. Its steps still propose their effects through the approval gate. The full logic is in Business logic.
play.simulate: dry-run replay over the last N days, with zero writes. Do this before you sign a play.play.trigger: start one run of a published play. It never bypasses approval.play.binding.listandplay.binding.set_status: see which plays run on their own trigger, and pause or resume one (owner or admin, human only).- Settings > Workflow automation turns the qualified-account handoff and signal reactions on or off (on by default).
Use a play when the steps are known and fixed. Use an agent when the steps depend on what it reads. Use a mission when a person must approve the plan first.
Pitfalls
- Your agent key cannot register, kill, revive or bump agents, create keys, set autonomy or approve a mission. Ask Claude for the exact call, then run it yourself in the console or with your owner session.
- Packs are off until someone turns them on. An empty Agents page is normal on a new org.
- A dev-only enable provisions no triggers. Enable in the default environment.
- A prod run reads only a signed
definition.agent. A draft is ignored there. - An agent never runs a paid enrichment or sends mail itself. It quotes, drafts and proposes.
- A mission does not advance by itself in 0.2.5. Someone must call
mission.step. - Do not mix up Mission Control (the home page), missions (
mission.*) and organization agents.
Governance and safety
Claude can do almost everything you can in the console. It cannot approve, sign, change its own authority or switch off a safety setting. This chapter shows exactly where the lines are: the autonomy ladder as it works in the code, approvals and four-eyes, environments, Dry Mode, spend limits, and the audit trail. Read it before you tell anyone something is live.
The control stack, in order
Goal: know every check an action passes, so you can say why something was allowed, drafted, held or refused.
Every operation takes the same path, whether it comes from the console, the CLI, the API or Claude over MCP. There is no side door. For a customer-facing effect (an email, an ad budget change, a page publish, a CRM write, a merge), these checks apply in this order:
| # | Check | What it can do | Set by |
|---|---|---|---|
| 1 | Who is calling | Refuse. Roles, key scopes, the human behind the agent. Some operations are human-only. | Roles and keys |
| 2 | The gate policy | Decide: allow, require_approval, draft or deny. With no policy, an effect needs approval. | Gate policy |
| 3 | The autonomy cap | Turn an allow back into require_approval when the autonomy level is below A3. | Autonomy ladder |
| 4 | Dry Mode and the engine's mode | Turn the action into a draft. Nothing runs, no approval is created. | Dry Mode |
| 5 | Guardrails and the net-new rule | Hold an action (quiet hours, a reached cap), or force a human approval. | Guardrails |
| 6 | The approval and four-eyes | A different human approves, then a human executes. | Approvals |
| 7 | The environment | In dev and staging, by default, the effect goes to a sink. Only prod fires live. | Environments |
Checks 3 to 5 can only make an action safer. They never undo what an earlier check tightened. The claims gate and the lane's pre-send checks also run on content and sends (see Claims, voice, proof and Lanes).
policy.get_dry_mode, policy.engine_modes.get, policy.guardrails.usage, policy.trust_holds.list and the approval's history with runtime.get_approval.Roles, keys and what Claude cannot do
Goal: know what your key can do and what only a person can do.
| Role | Permissions |
|---|---|
owner, admin | Read, write, configure engines, manage connections, approve. |
builder (delegated agency role) | Everything except approve. |
member | Read and write records. |
service | Write records. |
There is no viewer role. Only owner and admin approve, reject or revoke a proposal. Add a second approver in Settings > Members.
Agent keys. Create one in Settings > Developer or with gtm token create. It is shown once. The scopes are record.read (always), record.write, engine.configure and connection.manage. action.approve can never be granted to a key. Effective authority is the key's scopes, intersected with the agent's scopes, intersected with the creating human's current role, checked on every call. Demote the human and the key shrinks with them. Keys cannot mint keys.
Over MCP, every call counts as an agent call, even when your own login is behind it. So a human-only operation is done in the console (or with a person's own session on the API), never through Claude.
The human-only operations
An agent key is refused with FORBIDDEN "Operation ... can only be performed by a human". Claude prepares the values and tells you which screen to use.
| Reason | Operations |
|---|---|
| Four-eyes | runtime.approve_action, runtime.reject_action, runtime.execute_action, runtime.execute_approval, mission.approve, definition.sign, definition.sign_many, ontology.publish_changeset, delegation.approve, research.fact.decide, proof.propose_activation |
| No self-escalation | agent.register, agent.revive, agent.bump_version, agent.kill, agent.create_key, apikey.create, apikey.revoke, policy.set_gate_policy, policy.set_autonomy_scope, envelope.grant, envelope.revoke, delegation.grant, mcp_servers.set_tool_effects, roles.assign, roles.upsert, secret.rotate_org_key |
| Safety switches and policies | policy.set_dry_mode, policy.engine_modes.set, policy.guardrails.set, policy.claims.set, privacy.settings.set, score.learning_settings.set |
| Business settings a person owns | brand.set, money.settings.set, deal_type.set, reply_labels.set, buying_roles.set, territory.upsert, routing.member_settings.set |
| Go live | onboarding.launch |
| Browser consent | composio.oauth_start, composio.connect, connected_account.disconnect, and the Google, Microsoft and Slack sign-ins. Claude gives you the link. |
| Irreversible | org.offboard |
Field-level guards also apply: Claude cannot switch research autoApplyVerified on, and cannot pass toolEffects to mcp_servers.add.
Never silent. When a call is refused, Claude should tell you in the same reply: the operation, the error and the exact fix (the console path and button, a missing scope, or who approves). If it does not, ask.
operate is ordinary member-level work, build is engine configure, admin is approve). Ask Claude: "Which operations in this chapter would my key be refused, and why?" An expired, revoked or unknown key returns 401. There is no separate "expired" code, so ask for a new key.Approvals and four-eyes
Goal: know who can approve what, so nothing sits waiting.
Every effect, and every config change that goes live, becomes an approval in state proposed. It runs only after a human approves and a human executes it. Calling an effect through gtm_act creates the proposal and returns it. Claude does not call runtime.propose_action directly.
States: proposed, approved, rejected, withdrawn, revoked, expired, executing, executed, failed. A proposal expires after 7 days. Only the proposer withdraws one (runtime.withdraw_action).
Who can approve what
| Situation | Who can approve | Who cannot |
|---|---|---|
| Claude proposes an effect or a config change | An owner or admin who is not the human behind Claude's key | Claude. The proposer. The person who created the key (the server refuses it with "your own agent must be approved by a different person"). |
| You propose in the console | A different owner or admin | You. |
| Executing an approved action | A human who is neither the proposer nor the person whose agent proposed it | Any agent. The proposer. |
| Signing a definition | A different human, not the proposer and not the person behind the proposing agent | Any agent. |
A claim with no approved proof (in route_to_approver mode) | The approver acknowledges each flagged claim, then approves | A policy or envelope cannot auto-approve it. |
| An action auto-approved by a gate rule or an envelope | The standing rule is the approver. No person is asked. | n/a |
| Your organization has only one owner or admin | Nobody | Everything Claude proposes waits. Add a second approver first. |
The gate checks, in order, and each failure is FORBIDDEN: an agent cannot approve ("only a human can"); a role without action.approve cannot; the proposer cannot; the human an agent acts for cannot. At execution the gate checks the stored approver again. The console hides Approve only from the person who proposed. On an agent's proposal, the delegating human still sees the button and the server refuses it. That is expected.
Where people decide: Approvals (the full queue, with one approval's timeline) and Operate > Needs a human (pending effect approvals, config decisions with an impact preview, entity-resolution reviews).
What Claude should do after proposing
- Report the approval id, what it will do, and who can approve it.
- Not retry, not look for another path, and not ask the delegating human to approve.
- Read the result later with
gtm_approvalsorruntime.get_approval, and the trail withgtm_lineage.
runtime.get_approval returns one approval with its audit events and attribution: the proposer, the agent, the human it acted for, the approver.The autonomy ladder
Goal: understand what "autonomy" really means in GTM OS, what you can set, and what stays fixed. The product uses the words "the autonomy ladder", with five levels.
| Level | Name | Meaning in the code |
|---|---|---|
| A0 | Observe | Read and answer questions. |
| A1 | Suggest | Produce drafts and recommendations as records. No operations beyond reads. |
| A2 | Propose | Propose operations. Every effect needs approval. This is the default. |
| A3 | Act within policy | Effects may be auto-approved by a policy, within caps. Exceptions escalate. |
| A4 | Mission autonomy | Run a human-approved plan under an envelope, replan inside it, escalate outside it. |
What the ladder really does
- It applies to effects only. Reads, data writes and stage transitions are not affected.
- The disposition check tells only two groups apart: below A3 (A0 to A2) and A3 and above (A3 and A4). Below A3, an effect that a rule would allow becomes
require_approval. At A3 or above it may stayallow, but only if no human approvals are required for that scope. - Raising the level does not by itself turn on automation. Without a gate policy or an envelope that approves the action, an effect still needs a person, because the default for an effect is
require_approval. The level only removes one brake. To run without a person, a gate rule or an envelope must allow the action, and every other check in the control stack must pass too. - A first message to a net-new contact is never auto-sent. It always waits for a human approval, at every autonomy level, envelope and policy. This is a fixed rule, not a setting.
How a person sets it
policy.set_autonomy_scope is human-only. A scope is org (the organization default and ceiling), engine:<key> (one engine), or a narrower scope such as campaign:<id>, play:<key> or lane:<lane>. Each scope can set three things:
| Field | Meaning | Rule |
|---|---|---|
maxAutonomyLevel (0 to 4) | The ceiling. | A narrower scope can only lower it, never raise it. |
requiredApprovals | The minimum number of human approvals. | A narrower scope can only require more. |
autonomyLevel (0 to 4) | The level actually granted. | The narrowest scope that sets it wins. Never above the ceiling. Default A2. |
- A narrower scope may grant more than a broader one (that is the point of per-campaign autonomy), but never more than the ceiling. A write that would loosen a ceiling or an approval minimum is refused with
FORBIDDEN. - The outreach engine is guarded: its engine grant can never be higher than the organization's grant, even if you set it higher. Lower the org grant later and outreach drops with it.
- The cap applies only once the organization has at least one autonomy scope on record. With none, the check does nothing and every effect keeps the default approval path.
- The engines you can scope are the ones in the engine list below.
The easy way: the setup wizard's Autonomy per engine card (Setup, Rules). It offers A1 suggest, A2 approve each, and A3 act within caps, and sets each engine dry or live. It publishes through policy.engine_modes.set and policy.set_autonomy_scope, which a person approves. onboarding.launch then applies the approved card: it turns an engine live only if the card says so.
policy.get_autonomy_scopes with the scope keys ["org", "engine:outreach"] reads the saved rows. policy.evaluate tests what the gate policy would decide for given facts, without running anything.If it fails
FORBIDDENonpolicy.set_autonomy_scope: the scope would loosen the org ceiling or approval minimum, or a grant exceeds the ceiling. Lower the grant, or raise the ceiling at the org scope first.- "I set A3 and sends still need approval": expected. See above. Check the gate policy, the engine's mode, Dry Mode, and whether it is a first message to a new contact.
Gate policy and envelopes
Goal: for the rare case where you want an action to run without a person, know the two tools and their limits. Both are human-only and are not signed or promoted like definitions.
Gate policy
A gate policy is a table of rules. Each rule has a match (facts such as class, op, principalKind, plus facts an operation adds), a disposition (allow, require_approval, draft, deny), and optionally approvers, approvedBy (policy or envelope), quorum, expires and forbidPrincipalKinds. A rule that forbids agent turns an agent's attempt into deny. If no rule matches, the default applies: reads, data writes and stage transitions are allowed, and effects need approval. Set it with policy.set_gate_policy (each save is a new version, old versions are kept), read it with policy.get_gate_policy, and test it with policy.evaluate. Needs a human No console screen or CLI command exists for it in 0.2.5. A person calls the operation with their own session.
Envelopes
An envelope is a scoped, budgeted, time-bound pre-approval. envelope.grant (human-only) takes the grantee (granteeKind, granteeRef), the operations it covers as name patterns (for example enrich.*), an optional budgetEur, the window (windowFrom, windowTo) and whether it is revocable. envelope.revoke ends it at once. envelope.get and envelope.list_for_grantee read them. Activating a lane (lane.activate) grants that lane's spend envelope through the approval gate, sized by envelopeBudgetEur over windowDays.
- An envelope's
constraintsfield is stored but not enforced in 0.2.5. Do not rely on it. - A rule with
approvedBy: envelopeuses an envelope. A rule withapprovedBy: policyapproves on the policy's own authority. - The claims policy in
route_to_approvermode, and the net-new rule, override any auto-approval.
Environments: dev, staging, prod
Goal: test a change in dev, and make it live with one approval. Every organization has dev, staging and prod. prod is the default, and it is where the runtime reads: capability bindings, scheduled plays, the research agent, and every call that names no environment.
Which environment a call uses
| Surface | How to choose | If you leave it out |
|---|---|---|
| MCP | The environment argument of gtm_query, gtm_get, gtm_search and gtm_act, else the x-gtmos-environment header | prod. An environment that does not exist is refused (VALIDATION_FAILED), never silently prod. |
| REST | The x-gtmos-environment header | prod. Always name it. |
| CLI | --env <env> | The instance default, except that blueprint seeding goes to dev. The environment used is printed. |
| Console | The environment switcher in the header | prod |
The rules
- Definitions cannot be written straight into prod. Publish to a lower environment, sign, promote. See The full life of one definition.
- Effects. By default, a customer-facing effect in
devorstaginggoes to a sink: recorded, never sent. It is the same sink for every engine. Onlyprodfires live, after approval. - Promotion walks one step at a time.
env.promotelists every resource whose source version differs from the target, with its version and diff. Unsigned, deprecated and already-live ones are skipped with the reason. It proposes one approval, and on execution walks dev, staging, prod. Backward moves are refused. Re-running it proposes nothing new. A resource changed in dev after the approval is skipped, never silently promoted. - Named environments.
env.createadds one (key, name, stage, optionally its source environment). Owners and admins can, and so can an agent holdingengine.configure. - Stale change sets are refused with
CONFLICT. Re-propose, or the approver sees an explicitoverwrite. config.promotion_statusanswers "is my config live?": counts per environment, what sits in a lower environment but not in prod, and every definition awaiting a signature.
Always ask Claude to say which environment it read and which it wrote.
Dry Mode and engine modes
Goal: watch an engine think before it acts. There are two switches, and the stricter one wins.
| Switch | What it does | Set with |
|---|---|---|
| Dry Mode (the org master) | While on, every effect becomes a draft: logged, never executed, no approval created. A deny stays a deny. Reads, data writes, transitions and config changes are unaffected. It is org-wide and covers every environment. Every console page shows a banner while it is on. | policy.set_dry_mode (human-only). Read with policy.get_dry_mode. Console: Settings > Safety. |
| Engine mode (per engine) | dry: watch, score and draft, send and write nothing external. live: act, under the approval and autonomy rules. While the master is on, every engine stays effectively dry. The stored mode returns when the master is off. | policy.engine_modes.set (human-only). Read the stored and effective modes with policy.engine_modes.get. Console: Settings > Safety. |
New organizations start with every engine dry. A person switches each engine on, one at a time. An organization that existed before 0.2.5 inherits the master setting, so nothing changes for it unless someone sets an engine.
The engine keys are:
| Key | What it governs |
|---|---|
outreach | Emails and LinkedIn messages (outreach.*, sequence.*, actions.send_batch) |
meetings | Calendar invites the engines book or cancel |
perf_ads | Ad budget and status changes |
seo_geo | Content and meta changes published to tracked pages |
plays | Every action an automated play takes, on top of the engine that runs it |
routing | Slack and email notifications to owners |
crm_writeback | Scores, outcomes and field updates written back to the CRM and sources |
enrichment | Paid enrichment lookups |
identity_dq | Merges and unmerges of duplicate companies and contacts |
A dry engine keeps learning how good it would have been. policy.shadow_report shows, per engine, what it drafted while dry and, where it can be measured, how its predictions compared with what happened.
Show me the safety state
Show me the safety state of this organization: Dry Mode, the stored and effective mode of every engine, the guardrails and today's usage against each cap, and anything held. Then tell me exactly what I would click to take the outreach engine live, and who has to approve what afterwards. Change nothing.- Claude does
- Reads
policy.get_dry_mode,policy.engine_modes.get,policy.guardrails.get,policy.guardrails.usageandpolicy.trust_holds.list. - You do
- Needs a human Switch the engine in Settings > Safety when you are ready.
gtm.dry_draft.produced event is recorded each time. If "sends stay drafts", check Dry Mode first, then the engine's mode.Guardrails, holds and spend limits
Goal: cap what the engines can do per day and per week, and cap what they can spend. The settings are in Suppression and compliance. This is how they behave.
- Guardrails (
policy.guardrails.set, human-only): emails a day, accounts that get a first message each week, daily ad spend, a send window in the recipient's local time, allowed channels (email,linkedin,ads,sms,whatsapp), and up to 20 custom caps. A custom cap saysdefer(hold until the next window) orrequire_approval(only a human can go past it). The recipient's timezone comes from the contact, then the account, then the country, then your fallback. - Holds. An action held by quiet hours or a reached cap runs by itself when its time comes, and every check runs again then.
policy.trust_holds.listshows them.policy.trust_holds.cancelstops one. A hold never released expires after 14 days. - Consent and lawful basis checks refuse a send through the same path.
| Spend limit | Where it lives | When it is hit |
|---|---|---|
| Per-call data cap | Instance variable LOOOT_MAX_COST_USD, and a binding's maxUnitCostUsd | The run raises APPROVAL_REQUIRED and waits in Approvals. |
| Monthly enrichment cap | definition.enrichment_policy monthlyCapUsd | Lookups stop at the cap. |
| Lane budget | definition.lane envelopeBudgetEur over windowDays | "Envelope budget exhausted, re-approval required." |
| Play limits | definition.play guardrails.maxBudgetUsd, maxActionsPerRun | The play stops. |
| Model spend | definition.model_policy maxUsd per route | Per route. |
| Pre-approval budget | envelope.grant budgetEur | The envelope stops auto-approving. |
| Ledger budgets (org, environment, agent, play) | Internal | BUDGET_EXCEEDED (HTTP 402). The broker turns it into an approval proposal. |
No public operation sets an organization-wide spend budget in 0.2.5. Read spend with ledger.list and ledger.get (console: Operate > Cost), and price a call before you run it with capability.quote or enrichment.enrich_accounts with estimate: true.
Audit, lineage and the kill switch
Goal: see who did what, trace any value back to its source, and stop an agent now.
| You want | Use |
|---|---|
| Every operation that ran, tamper-evident | audit.list (hash-chained, up to 500 per call). Console: Settings > Audit log. |
| One approval's history and attribution | runtime.get_approval, or gtm_approvals and gtm_lineage |
| Where a field value came from | lineage.trace (gtm_lineage_trace): the value, the survivorship rule, the contributing observations. records.provenance gives per-property evidence. Derived and metric lineage are empty in 0.2.5. |
| What was spent, and by what | ledger.list. Console: Operate > Cost, drill from a line to the run. |
| Stop one key | Settings > Developer, or gtm token revoke <key-id> |
| Stop an agent everywhere | Settings > Agents then Kill (agent.kill). agent.revive restores it. Both are human-only. |
An agent's own limits live in definition.agent: authorityScopes, skillRefs, and hard run bounds (maxSteps, maxCostUsd, deadlineMs). A run stops at a bound. See Agents, skills and workflows.
Prompts for this chapter
Goal: check state and prepare changes. Claude cannot flip a safety setting or approve. These prompts show you what a person must do.
Move this definition to prod
Move definition.scoring_model default from dev to prod. First check that the dev version is signed and tell me who may sign it. Then preview the promotion and list everything it would carry, including things I did not mean to ship. Stop before you propose the promotion, and tell me who has to approve it.- Claude does
- Reads
config.historyandconfig.promotion_status, thenenv.promotewithpreview: true(it plans every resource, not just one). - You do
- A different human signs. Then tell Claude to propose the promotion with
env.promote(orconfig.promotefor one resource), and a different human approves and executes it.
What is waiting for approval, and why
Show me everything waiting for approval: what each one will do, who proposed it, how long until it expires, and who is allowed to approve it. Flag anything I cannot approve myself and why.- Claude does
- Reads
runtime.list_approvalsandruntime.get_approval(the same asgtm_approvals). - You do
- Approve what you may in Approvals. Send the rest to the right person.
Why was this held?
This email was not sent. Tell me why: Dry Mode, the engine mode, a guardrail or quiet hours, the net-new rule, a claims check, a lane check, or the environment. Show the evidence for the one that applies.- Claude does
- Walks the control stack with the read operations above and names the check that applied.
- You do
- Fix the cause, or accept it.
Prepare to go live with one engine
I want the outreach engine live with a daily cap of 50 emails and quiet hours outside 08:00 to 18:00 recipient time, Monday to Friday. Tell me the current guardrails and the autonomy setting for outreach. Draft the exact guardrails spec I should save and the engine mode change I should make. Do not change anything.- Claude does
- Reads the current guardrails and engine modes and drafts a complete
policy.guardrails.setspec (it replaces the whole spec) and thepolicy.engine_modes.setchange. - You do
- Needs a human Save the guardrails and switch the engine on in Settings > Safety.
Prompts in this guide are untested drafts. Run each once on a dry-mode instance before you rely on it.
Extend: apps and packages
When the built-in engines and screens are not enough, you extend GTM OS in a governed way. You ask Claude, Claude proposes, a different person approves. Nothing here changes the kernel.
Untested prompts. The prompts in this chapter were not run against a live instance. Operation names were checked against the 0.2.5 catalog. Run each once on a dev instance in Dry Mode before you rely on it.
Pick the smallest extension that works
Goal: avoid building a package when a definition would do.
| You want | Use | Section |
|---|---|---|
| A new screen: table, queue, board, record page, dashboard | A declarative app (definition.app) | Apps |
| A new tile on the home page | A metric and a Mission Control tile | Mission Control tiles |
| "Describe what you wish existed" and let GTM OS draft it | Studio | Studio |
| New data from an app or internal tool | A source mapping or an MCP server binding | Connector builder |
| Agents, skills, triggers, metrics, policies or apps shipped together, versioned and reusable | A declarative package | Packages |
| Custom interface code in the console | A sandboxed code app in a package | Sandboxed code apps |
Prefer a declarative package of definitions over code. Everything that runs code needs a signature.
Declarative apps
Goal: a new screen under Apps > All apps, opened at /org/<orgId>/a/<appKey>, built from a definition and no code.
A definition.app has a label, audiences (who sees it, optionally by role), routes (path to page), pages and an optional nav. Each page is one view type:
| View type | Use |
|---|---|
queue | A list of work with row actions. |
table | A table of records. |
record | One record with fields, related lists and actions. |
board | Cards by state, with allowed transitions. |
dashboard | Metric tiles, a spotlight and ranked lists. |
form, approval_card, wizard | Input, an approval view, and a multi-step flow. |
Apps can only act through registry operations. There is no link, script or URL field, so an app can never do something an operation does not allow. Cell formats are text, badge, number, datetime, currency, label.
Scaffold one from the live registry: gtm app generate <objectType> --kind record_page|queue|table|dashboard [--metrics key:Label:format,...] [--out file]. The output is valid against the app schema and uses operations only.
Add a screen
Build a table app of open deals by stage with value and owner, and a queue of accounts that need review. Scaffold it from the object types, validate it, and propose it. Tell me which operations each view calls.- Claude does
- Runs the scaffold, wraps the spec as a
definition.appresource, thenconfig.propose_changeset,config.validate_changeset,config.apply. - You do
- Needs a human A different person approves. Sign it (
definition.sign). Promote withenv.promoteif prod should have it. - You should see
- The app under Apps > All apps, opening at its
/a/<appKey>route, with live data.
Mission Control tiles
Goal: your own number on the home page.
Mission Control is itself a definition.app (key mission-control) with a dashboard view. A tile is a label, a metric key, a source and a format:
- label: Open pipeline
metricKey: open_pipeline
source:
operation: metric.evaluate
method: GET
label: Open pipeline
input: { key: open_pipeline }
format: currency # currency | percent | numberSo a new tile needs two things: a definition.metric (an aggregate over the ontology with a filter and a grain), and a tile that points at its key. A tile whose metric is not published shows a dash, never a made-up number. The same page also has a spotlight ("Today's one move": the oldest pending approval) and lists (ranked risks from monitors.list, the overnight engine log from runs.list).
Add a tile
Add a Mission Control tile "Hot accounts without an owner": a metric that counts accounts with a qualified score and no owner. Show me the metric definition and the changed tile list first. Propose both together.- Claude does
- Checks existing metrics (
metric.evaluateon a candidate key), draftsdefinition.metric, reads the current app withconfig.get, and proposes one ChangeSet with the metric and the editedmission-controlapp. - You do
- A different person approves and signs. Promote to prod.
- You should see
- The tile on the home page with a number. Run
metric.evaluatefor the key to prove it returns a value.
Updates and your edits. A GTM OS upgrade never changes an installed engine app by itself. Settings > Packages shows "Engine app updates". An owner or admin proposes the update, your edits are merged in, and if an edit overlaps the release that app is not updated and you choose Keep mine or Take new. Every update is a new config version you can roll back with config.rollback. apps.update is humans only. Claude can read apps.update_available.
Studio: describe it in a sentence
Goal: a request for a new play or app becomes a drafted spec, numbered questions and a governed ship.
| Step | Operation |
|---|---|
| Ask | studio.request. GTM OS drafts the spec and turns each ambiguity into a numbered open ask. |
| Answer, then re-draft | asks.answer, studio.draft_spec |
| Edit by hand | studio.update_spec (a person) |
| Ship | studio.submit builds a definition.play or definition.app ChangeSet, validates it, and proposes shipping. A person other than the proposer approves. |
| Vote, cancel, read | studio.vote, studio.cancel, studio.get, studio.list |
Console: CRM > Studio. The spec shows data needed, approvers, an estimate and a trust level.
Packages: build, sign, install
Goal: ship your own bundle (agents, skills, triggers, apps, connectors, ontology modules) as a versioned package.
| Tier | Runs | Signature |
|---|---|---|
declarative | No code: definitions, apps, packs, blueprints | Optional, verified if present |
trusted | Code in the platform process | Required and verified |
sandboxed | Code in an isolated sandbox | Required and verified |
An unsigned or unverifiable trusted or sandboxed package is refused at install with FORBIDDEN. Signatures are Ed25519 or sigstore keyless. The trust set is the first-party key plus GTMOS_TRUSTED_PUBLISHER_KEYS on the instance. A community package may run only sandboxed and outside prod.
The manifest (gtmos.package.yaml) has name (scope/name), version, tier, optional kernel range, requires (packages, capabilities), contributes, permissions (read, write, operations, egress), parameters, evals and signature.
gtm pkg scaffold acme/sales-pack --tier declarative # starter gtmos.package.yaml
gtm pkg validate gtmos.package.yaml # lint, offline
gtm pkg test gtmos.package.yaml # declared eval files exist
gtm pkg build gtmos.package.yaml --out dist # hash + SBOM -> .gtmpkg.json
gtm pkg sign gtmos.package.yaml --generate-key # Ed25519, offline
gtm pkg install dist/<artifact>.gtmpkg.json # PROPOSES; needs GTMOS_API_URL
gtm pkg enable acme/sales-pack --env dev # PROPOSES, per environmentScaffold and validate a package
Scaffold a declarative package acme/outbound-pack that contributes a scoring model, a lane and a play, with the permissions it needs and nothing more. Validate it and build the artifact. Show me the diff between its manifest and what is installed. Do not install.- Claude does
gtm pkg scaffold,validate,build, thenpackages.preview_upgrade(a read-only diff, never installs).- You do
- Needs a human Install and enable. Each is a proposal a different person approves in Approvals.
- You should see
- Settings > Packages lists it under Installed, enabled in the environments you chose.
gtm doctorprints the exact enable command for an engine package that is missing.
Upgrade and rollback: gtm pkg upgrade <name> [--to v] [--dry-run] [--yes], packages.rollback. Uninstall with --purge removes the config the package owns. Your records are never deleted. A breaking change needs allowBreaking.
Publishing
Goal: know what "publish" means for you and what it does not.
- Publish a definition: draft, propose, sign (a different person), promote. See Definitions as code.
- Publish a package for your own org: build, sign if code, then
gtm pkg installandenable. That is the supported route. - Local registry:
gtm pkg publish <artifact> [--registry <dir>]writes to a local file registry.gtm pkg registry listreads it.gtm pkg lockwrites apackages.lock.gtm pkg provenance --org <id>shows which agency package and version an org runs. - Hosted registry:
pkg.publish,pkg.verify,pkg.promote,pkg.yankand the entitlement operations need thepackage.publishorpackage.entitlescope. That is the platform and agency plane. A normal agent key does not hold it. Search and resolve (pkg.search,pkg.resolve,pkg.list) are open reads.
Nothing you publish may contain readable source for others. Build artifacts hold hashes and an SBOM, and code tiers must be signed.
Sandboxed code apps
Goal: custom interface code that cannot reach the console, its cookies or its storage.
A package can ship a code app: an ES module rendered in the console. A sandboxed app runs in an isolated iframe with an opaque origin, a strict content security policy and no powerful browser features. It has one capability, the bridge: sdk.callOperation(name, input). The console runs the call only if the operation is listed in permissions.operations. Anything else returns FORBIDDEN. Calls run in the viewer's org. Input is capped at 256 KB and 16 concurrent calls.
name: acme/pipeline-widget
version: 1.0.0
tier: sandboxed
contributes:
apps:
- https://cdn.acme.example/pipeline-widget/1.0.0/app.js # the only bundle that may load
permissions:
operations: [records.search, records.get] # deny-all when absent
egress: [api.acme.example] # hostnames or *.suffix only- The bundle must be served over
https(httponly onlocalhost) withAccess-Control-Allow-Origin: *. - Network access is limited to
permissions.egressand is denied by default. - The module exports
mount({ container, sdk, organizationId, theme })and may return a cleanup function. - A
trustedcode app runs in the console origin and must be signed. Usesandboxedfor anything third-party or unsigned. Adeclarativepackage cannot ship code apps.
The connector builder: three ways to add a data connector
Goal: add a source or capability without writing platform code.
| Way | When | How |
|---|---|---|
| Adapter builder | The app is on Composio, or you have an MCP server with a read list tool, and you want records in the ontology. | Introspect, audit, propose, answer, publish, sign, activate. See The adapter builder. |
| Your own MCP server | You can write a small server. You want GTM OS to call it as a capability (lookups, scores) or as a source. | Add it, mark each tool Read or Write, bind a capability. See MCP servers. Build the server with your coding agent and the MCP builder skill. |
| Code connector package | The built-in connectors (looot, Resend, Unipile, GSC, GA4, Cal.com, webform and others) are packages of this kind. | These are first-party trusted packages. A customer-built one needs a signature from a key in the instance trust set. Start with the two ways above first. |
Build a connector to an internal system
We have an internal billing API at https://billing.acme.example. Design a small MCP server with one read tool, list_invoices, and one read tool, get_customer. Then plan how to land invoices as custom object Invoice through an adapter. Give me the server code, the tool list, the mapping plan and the approvals each step needs. Do not register anything yet.- Claude does
- Writes the server in your repo, not in GTM OS. Plans
ontology.objectfor Invoice, an adapter session and the sign steps. - You do
- Host the server. Register it. Mark its tools Read. Approve the object and the mapping. Sign.
- You should see
- The server Connected with two tools, the Invoice object in CRM > Model, and an adapter in CRM > Adapters with a confidence on every line.
Verify and pitfalls
- New app: it appears under Apps > All apps and renders with data.
- New package: Settings > Packages shows it enabled in the right environment, and
gtm doctorhas no warning for it. - New tile:
metric.evaluatereturns a number for its key. - Every change is a config version. You can roll it back with
config.rollbackorpackages.rollback.
- Install and enable are proposals. Until a different person approves, nothing changed.
- Editing a shipped engine app is allowed, but a later release may overlap your edit. You then choose Keep mine or Take new.
- A sandboxed app cannot call an operation that is not in its manifest, and cannot fetch from a host that is not in
egress. - Do not publish a package that lists operations it does not need. Reviewers read that list first.
Operate the instance
Keep your instance healthy, reachable, backed up and current. An upgrade never touches, removes or loses your data. Every command here is copy and paste, and each one says what you should see.
What runs, and what is in the folder
Goal: know the moving parts, so a health check or a restart makes sense.
| Service | What it does | Default port |
|---|---|---|
console | The web app you and your approver use. | 3000 |
api | The REST API. gtm talks to it. | 3001 |
mcp | The MCP server Claude connects to (path /mcp). | 3004 |
worker | Runs jobs: scans, syncs, scheduled plays. | none |
postgres | Your database. | 5432 |
migrate | A one-shot task that updates the database shape at start. It ends with "Exited (0)". That is normal. | none |
The folder holds a generated docker-compose.yml, your .env (with your secrets), and, after your first upgrade, a backups/ folder. The database lives in a Docker volume named gtmos_postgres_data.
gtm uprewritesdocker-compose.ymlevery time it runs. Never edit that file to change ports or settings. Edit.envinstead.gtm upnever overwrites an existing.envunless you pass--force, and--forcerotates your generated secrets. Do not use it on a live instance.
docker compose down -v on your instance. The -v flag deletes the database volume. Plain docker compose stop and docker compose up -d are safe.Check that it is healthy
Goal: a two-minute check you can run any time, or ask Claude to run.
curl http://127.0.0.1:3001/health
curl http://127.0.0.1:3004/health
docker compose --env-file .env -f docker-compose.yml ps
gtm doctor{"status":"ok", ...} with the running version (the MCP one also says "service":"mcp"). ps shows migrate as Exited (0) and the others Up or healthy. gtm doctor prints one line per provider (LIVE, UNVERIF, FAILED, FIXTURE or MISSING) with the fix. gtm doctor --strict exits with code 1 if anything you need is not live, which makes it a go-live gate.The MCP server also answers /ready. It checks the database and migrations, and returns 200 when ready and 503 when not.
Open the console at http://localhost:3000. Type localhost, not 127.0.0.1: the sign-in cookie is set for the console's public address, and signing in through 127.0.0.1 can hang on "Signing in..." with no error.
Ports are local only by default
Goal: know who can reach the instance, and open it on purpose when you need to.
127.0.0.1 by default, like Postgres. An older instance becomes local-only on its next gtm up or gtm upgrade. If you only use GTM OS on one machine, nothing changes for you.GTMOS_BIND_ADDRESS in .env | Meaning |
|---|---|
127.0.0.1 (default) | Reachable from this machine only. Use it with a reverse proxy on the same machine. |
0.0.0.0 | Reachable on every network interface. Only behind a firewall that allows exactly the ports you intend. |
A private IP such as 10.0.0.5 | Reachable on that interface only, for a proxy on another host in the same private network. |
To change it, edit GTMOS_BIND_ADDRESS in .env and run gtm up again, or create the instance with gtm up --bind 0.0.0.0. A --bind flag passed to an existing instance is ignored and reported, because the existing .env wins. gtm up prints the effective address and warns when it is 0.0.0.0.
Coming from an older instance? The first gtm up or gtm upgrade on 0.2.5 warns when .env has no GTMOS_BIND_ADDRESS, then writes 127.0.0.1 into it. If a teammate, a webhook or an MCP client on another machine reached the instance by IP address, use a reverse proxy or 0.0.0.0.
Self-host on a server with a domain and HTTPS
Goal: people reach GTM OS at https://app.acme.example while the instance stays closed to everything except your proxy.
- DNS and firewall. Needs a human Point
app.acme.example(console),api.acme.example(API) and, if coding agents outside the server need it,mcp.acme.exampleat the server. Use one parent domain so the login cookie is shared. Open only ports 80 and 443. Do not open 3000, 3001, 3004 or 5432. - Keep
GTMOS_BIND_ADDRESS=127.0.0.1. Set the public addresses in.env. They are the origins a browser really uses:https://, no trailing slash. Then rungtm upagain.PUBLIC_API_URL=https://api.acme.example PUBLIC_CONSOLE_URL=https://app.acme.example PUBLIC_MCP_URL=https://mcp.acme.example - Proxy each host to its local port. With Caddy, which gets and renews HTTPS certificates for you, the whole
Caddyfileis:nginx works the same way: oneapp.acme.example { reverse_proxy 127.0.0.1:3000 } api.acme.example { reverse_proxy 127.0.0.1:3001 } mcp.acme.example { reverse_proxy 127.0.0.1:3004 }serverblock per host withproxy_passto its local port, theHostandX-Forwarded-Protoheaders, and theUpgradeheaders for the console. - Check it (the last line is for Linux):
curl https://api.acme.example/health curl -I https://app.acme.example ss -tlnp | grep -E ':(3000|3001|3004|5432)\b' - Point Claude at the public MCP address. In the workspace, set
GTMOS_MCP_URL=https://mcp.acme.example/mcp, or rungtm init --refresh --url https://api.acme.example --key <agent key> --yes.
ok, the console answers 200 or redirects to the login page, and every line of the last command shows 127.0.0.1, never 0.0.0.0 or *.Common errors
- Login redirects loop or cookies are not stored: the public addresses in
.envdo not match what the browser uses, for examplehttp://in.envandhttps://in the browser. - Nothing answers by IP address after an upgrade: the ports are local-only now. See the previous section.
- Claude gets 401 from the public MCP address: the key is wrong or expired, or the proxy strips the
Authorizationheader.
The settings in .env that matter
| Setting | What it does |
|---|---|
GTMOS_VERSION, GTMOS_REGISTRY | The release channel or pinned version the images come from, and the registry. A pinned version stays pinned until you name a new target (see Update to a newer release). |
GTMOS_BIND_ADDRESS | Which network address the console, API and MCP ports bind to. Default 127.0.0.1. |
PUBLIC_API_URL, PUBLIC_CONSOLE_URL, PUBLIC_MCP_URL | The addresses a browser or Claude really uses. Change them only for a server with a domain. |
GTMOS_LIVE | Must be true or false in production. The API, worker and MCP images refuse to start without it. true: an unconfigured model, connector or sender fails with NOT_CONFIGURED. false: visibly fake demo data. New installs write true. An upgrade writes false into an older .env, which is how it already behaved. Set true and restart the API and worker to go live. |
GTMOS_MASTER_KEY | Seals every stored credential. Backups do not contain it. Keep a copy somewhere safe, apart from the dumps. |
GTMOS_PLATFORM_CREDENTIAL_PROVIDERS | Which instance-level keys in .env an organization may fall back to when it has no connection of its own. Setting it replaces the default list. |
GTMOS_OWNER_PASSWORD, GTMOS_APPROVER_PASSWORD | The generated sign-in passwords from gtm setup. Share the approver's privately, then change them in the console if you like. |
GTMOS_INSTANCE_ADMIN_EMAILS | Who may update the whole instance from Settings > Updates. gtm setup sets it to the owner. |
EMAIL_TRANSPORT and SMTP_* | How the instance sends invitations and password reset mail. Unset means no system mail is sent. |
After you edit .env, run gtm up so the containers are recreated with the new values. Then run /mcp in Claude Code to reconnect.
Back up
Goal: a copy of your database you can restore from, and your master key kept apart from it.
The backup every upgrade takes
Every gtm upgrade stops the API, worker, MCP server and console, then writes a full backup of the database (every organization in it) as backups/gtmos-pre-upgrade-<stamp>.dump. It reads the file back to prove it is readable. It also keeps .env.pre-upgrade-<stamp> and docker-compose.yml.pre-upgrade-<stamp> next to it. The newest five are kept. The backup is never restored automatically.
Take your own backup
There is no gtm backup command in 0.2.5. Use the same pg_dump the upgrade uses, run inside the Postgres container. Run it in a bash or zsh shell. In Windows PowerShell the > redirect corrupts a binary file.
mkdir -p backups
docker compose --env-file .env -f docker-compose.yml exec -T postgres pg_dump -U gtmos -d gtmos -Fc --no-password > backups/gtmos-manual.dumpbackups/gtmos-manual.dump file larger than zero bytes. Check it is readable with docker compose --env-file .env -f docker-compose.yml exec -T postgres pg_restore --list < backups/gtmos-manual.dump, which prints the contents list.On Windows PowerShell, write the file inside the container and copy it out instead:
docker compose --env-file .env -f docker-compose.yml exec -T postgres pg_dump -U gtmos -d gtmos -Fc --no-password -f /tmp/gtmos-manual.dump
docker compose --env-file .env -f docker-compose.yml cp postgres:/tmp/gtmos-manual.dump ./backups/gtmos-manual.dumpKeep the master key apart
A dump does not contain GTMOS_MASTER_KEY. Every stored credential (connections, OAuth tokens, SSO secrets) is sealed with that key, so a restored database without it cannot read them. Copy the key from .env into a password manager when you install, and again if you ever change it. Keep it somewhere other than next to the dumps. The dump itself is not encrypted and contains personal data, so protect the folder it lives in.
Export one organization
gtm org export --org <orgId> --out ./acme-export --format parquet --dry-run
gtm org export --org <orgId> --out ./acme-export --format parquet- It writes a portable bundle for one organization: one Parquet file per table (default), or with
--format sqlone file of INSERT statements. A manifest lists each table with its row count. - With
--env <env>andGTMOS_API_URLset, it also writes that environment's configuration (your definitions and settings). - It connects as the restricted application database role (
DATABASE_URLor--database-url), so it can read only that organization's rows. It only reads.--dry-runprints the plan and writes nothing.
Update to a newer release
Goal: move your instance to a newer release without losing anything. Every release only adds (new tables, nullable or defaulted columns, indexes). The updater refuses any release that does not, and there is no override.
Which version am I on?
The CLI version and the instance version are separate. gtm upgrade --check shows both, plus what the signed release list offers, and changes nothing. Releases before 0.2.5 published images only as 0.2.2 and stable. CLI versions 0.2.3 and 0.2.4 shipped no images, so an instance installed with either still runs the 0.2.2 images.
- Upgrade the CLI first. It renders the compose file your instance runs on, so it must be at least as new as the release you move to. If it is too old,
gtm upgradestops and says which version to install.npm i -g @gtmos/cli@latest - See what is available:
gtm upgrade --check - Rehearse. It prints every command and changes nothing:
gtm upgrade --channel stable --dry-run - Upgrade.
--yesis required because it migrates your live database:gtm upgrade --channel stable --yes - In Claude Code, run
/mcpto reconnect. Then, in each project folder, take the newest skills and subagents (files you edited are kept):gtm init --refresh
UPGRADE OK. A plain gtm upgrade on a pinned install ends with RE-VERIFIED <version>, NOT UPGRADED instead.gtm upgrade never moves a pinned install. With neither --channel nor --to, it only re-pulls and re-verifies the release you already run. To move, name the target: --channel stable, or --to <version> for an exact release.What the upgrade does, in order
- Fetches the signed release list and checks its signature. It plans the path by the list's rules: no downgrade, no skipped required stop, additive-only releases, a minimum CLI version. Any failed check refuses the upgrade before anything is pulled.
- Preflight. Only one update at a time. Free disk must be at least twice the database size plus 2 GB.
- Pulls the new images. Each image's digest must match the signed digest.
- The additive-only guard reads every pending migration and refuses anything that drops, renames, retypes, deletes or overwrites.
- Stops the API, worker, MCP and console gracefully, and takes a backup that it reads back to verify.
- Rehearses on a scratch copy: it restores that backup into a scratch database, migrates it and compares it with your live data (tables, columns, types, row counts, row checksums). Any difference refuses the upgrade.
- Pins the version in
.env, runs the migration inside the new image, starts the services, and verifies health and schema.
What is preserved
- All your data: records, history, definitions, approvals, connections and keys.
- Your apps and configuration. An upgrade changes no installed engine app. An owner or admin proposes an app update in Settings > Packages, a second person approves it, and your own edits are merged in.
- Your
GTMOS_MASTER_KEY. Keep your own copy.
Moving to 0.2.5 from 0.2.2, or from CLI 0.2.3 or 0.2.4 on 0.2.2 images
- Upgrade the CLI first. This is not optional.
- Name the target. An install set up with
--version 0.2.2is pinned: rungtm upgrade --channel stable --dry-run, thengtm upgrade --channel stable --yes. - Ports become local-only. See Ports are local only by default.
- Read the settings table:
GTMOS_LIVE, instance keys, who can import CSVs. - Account scores can move once on the first rescan. Preview it before it runs: ask Claude to run
score.rescan_preview, which writes nothing. - Run
gtm init --refreshin each project folder. - Already on a newer 0.2.x? The path is the same. If a release in between is a required stop,
gtm upgradeadds it as an extra hop and takes a backup at each hop.
Check it worked
gtm doctorshows the sources you expect as live.- The two health calls from Check that it is healthy report the new version.
- The upgrade said verification passed, and a snapshot is in
backups/. - Row counts of your main tables match what you noted before.
- Dry Mode send check: propose one send to an internal address and confirm that a different person has to approve it.
.claude/agentsin each project holds the role subagents aftergtm init --refresh.
One-click updates from the console (optional)
Install the updater service once, in the instance folder. It registers a small background service on this machine that applies updates you request, with a backup first:
gtm updater install
gtm updater statusgtm updater status prints registered yes and running yes (pid ...). Instance administrators then see an update button in Settings > Updates. Remove it with gtm updater uninstall. Skip the service and use gtm upgrade if you prefer.Roll back
Goal: get the previous release back. You almost never need a database restore.
When an upgrade fails
It rolls back by swapping the images back. The previous compose and .env files and the previous images are restored, and the services restart on them. The database is never restored, dropped or recreated by an upgrade. The verified backup stays in backups/. This works because every migration only adds things, so the previous code runs on the migrated database.
gtm upgrade --check and the two health calls. They tell you which release is running.Go back after a successful upgrade
Use the oldest stamp from that upgrade run (one upgrade may take several hops). From the instance folder:
docker compose --env-file .env -f docker-compose.yml stop api worker mcp console
cp .env.pre-upgrade-<stamp> .env
cp docker-compose.yml.pre-upgrade-<stamp> docker-compose.yml
docker compose --env-file .env -f docker-compose.yml up -d
gtm upgrade --checkUse docker compose here, not gtm up. gtm up writes the compose file again from the installed CLI and replaces the copy you just restored. The previous images must still be in your local cache or on the registry, and a pulled image must match its signed digest, so do not remove old images until you are sure.
gtm upgrade --check names the previous release as the one your instance runs.Restore a backup (last resort)
Only if something other than an upgrade damaged your data. Never restore over the live database. Keep the current one by renaming it, then restore into a new one. gtm upgrade --help prints the exact commands. In short:
- Stop the services and rename the live database so nothing is lost:
docker compose --env-file .env -f docker-compose.yml stop api worker mcp console docker compose --env-file .env -f docker-compose.yml exec -T postgres psql -U gtmos -d postgres \ -c "ALTER DATABASE gtmos RENAME TO gtmos_before_restore_<stamp>" docker compose --env-file .env -f docker-compose.yml exec -T postgres createdb -U gtmos -O gtmos gtmos - Restore in three sections, with one repair between the first two (a function that a 0.2.2 check constraint calls needs its search path set). Use the dump you chose:
docker compose --env-file .env -f docker-compose.yml exec -T postgres pg_restore -U gtmos -d gtmos --section=pre-data < backups/gtmos-pre-upgrade-<stamp>.dump docker compose --env-file .env -f docker-compose.yml exec -T postgres psql -U gtmos -d gtmos \ -c "ALTER FUNCTION public.is_valid_record_link_object_type(uuid, text) SET search_path = public" docker compose --env-file .env -f docker-compose.yml exec -T postgres pg_restore -U gtmos -d gtmos --section=data < backups/gtmos-pre-upgrade-<stamp>.dump docker compose --env-file .env -f docker-compose.yml exec -T postgres pg_restore -U gtmos -d gtmos --section=post-data < backups/gtmos-pre-upgrade-<stamp>.dump - Put
.envanddocker-compose.ymlback from their.pre-upgrade-<stamp>copies, thendocker compose --env-file .env -f docker-compose.yml up -d. The renamedgtmos_before_restore_<stamp>database stays until you drop it yourself.
Restoring a dump also needs the original GTMOS_MASTER_KEY, or the stored credentials cannot be read.
Verify a release yourself
- Releases are announced in a signed list,
releases.json, with an ed25519 signature next to it. The CLI trusts only the release keys built into it. The default address ishttps://ay-automate.github.io/gtmos-releases/releases.json, andGTMOS_RELEASE_MANIFEST_URLoverrides it. An air-gapped site serves a mirrored list that is still signed. - For each release the list gives the version, date and channel, whether it is a security release, the minimum CLI version, the oldest version it can be reached from, any required stops, whether it is additive-only, and the sha-256 digest of the api, worker, mcp and console images.
gtm upgrade --checkfetches and verifies the list and prints what it offers. It changes nothing. Before an upgrade changes anything the CLI also checks that the images come from an allowed registry (ghcr.io/ay-automateunless you mirror), and after the pull that every digest equals the signed one.
Your own cloud (BYOC)
gtm byoc plan|apply|destroy --cloud aws --region <region> provisions one GTM OS instance in your own AWS account with Terraform (or OpenTofu 1.6 or later): managed Postgres, the four services, an object bucket, Secrets Manager and a load balancer. Honest scope: AWS is the only cloud. plan works offline without cloud credentials and shows what it would create. apply and destroy refuse to run without --yes, real credentials and Terraform. The installer and its dry run are built and tested. A live deployment into a real AWS account has not been done yet.
Prompts for this chapter
Check for updates and plan the upgrade
In the instance folder, run gtm upgrade --check, then gtm upgrade --channel stable --dry-run. Tell me which version I run, which version the stable channel points at, whether my CLI is new enough, and what the upgrade would do. Do not run gtm upgrade with --yes. Wait for me to say go.- Claude does
- Runs the two read-only commands and summarizes them.
- You do
- Take a manual backup, then tell Claude to go ahead (or run the real upgrade yourself).
Run a health check
Check that my GTM OS instance is healthy: call the API and MCP /health endpoints, show docker compose ps for the instance folder, and run gtm doctor. Summarize anything that is not OK and tell me the fix. Change nothing.- Claude does
- Runs the four checks from Check that it is healthy.
- You do
- Fix what it names.
Take a backup
Take a manual backup of my GTM OS database into the backups folder of the instance, then check that the file is readable with pg_restore --list. Remind me that GTMOS_MASTER_KEY is not in the dump and must be stored separately. Do not print the key.- Claude does
- Runs the
pg_dumpandpg_restore --listcommands from Back up. - You do
- Copy
GTMOS_MASTER_KEYfrom.envto your password manager.
If something looks wrong
Upgrade problems
- The upgrade refused before changing anything: read the message. It names the failed check (signature, path rule, CLI too old, disk, a migration that is not additive).
- It failed part way: the images were swapped back and your backup is in
backups/. Rungtm doctorand the health calls to confirm what is running. - You need data from a backup: follow the manual steps above, and do not skip renaming the live database.
- Claude lost the connection after a restart: run
/mcp.
Prompt recipes
Copy a prompt, replace the parts in angle brackets, paste it into Claude Code. Each recipe says what Claude does, what you do, and where to read the details.
Replace anything in <angle brackets> before you send. Work in dev unless a recipe says otherwise. Claude never approves its own work, so every recipe that changes live behavior ends with a person.
Find a recipe
Start
Stand up an org from a blueprint
We sell <one sentence: who, what, how>. Compare the ten blueprints with blueprint.list and blueprint.get and recommend the closest one in three lines. Check what it needs with blueprint.requirement_status. Then apply it to dev with blueprint.apply as a proposal. If it is a search blueprint, ask me for the Search Console property before you apply. List what you would change for us. Do not promote anything.- Claude does
- Reads the catalog, recommends one (for example
enterprise-abmfor named accounts with buying committees), previews the requirements, then proposes the apply. - You do
- A different admin approves the proposal in Approvals. Connect what the blueprint needs in Integrations > Connections.
- Read more
- Blueprints, Life of a definition
Check your connection and your key
Check the gtmos connection. Call agent.whoami, policy.get_dry_mode and instance.readiness. Tell me who you act as, your scopes, whether you can approve, whether Dry Mode is on and which providers are live. List what you cannot do without a person. Change nothing.- Claude does
- Runs three reads through
gtm_queryand maps each blocked task to the person who must do it. - You do
- Fix anything not live in Integrations > Connections. Mint a wider key if a scope is missing.
- Read more
- Connect Claude Code, Effective authority, If the connection fails
Run the setup wizard by prompt
Set up GTM OS for my company. Read where setup stands first. Use my website to draft what you can, show the evidence for every value, and save only what I confirm. Turn every gap into a numbered open ask. Stop before onboarding.launch.- Claude does
- Reads
onboarding.get, resumes at the current step, drafts cards with evidence and saves only confirmed answers. - You do
- Answer, upload documents, finish the human-only cards. Launching the build is Needs a human.
- Read more
- The setup wizard, Build the system
Business logic
Define the ICP and the scoring model
Our ICP: <industry, size range, regions>. Strong signals: <list>. Never score: <competitors, regions>. Read our current default and icp_fit scoring models in dev. Show the old and new specs side by side. Validate, run score.rescan_preview, and propose the change set. Work in dev. Do not rescan the organization.- Claude does
- Reads both models with
config.get, drafts specs, validates, previews who moves into or out of qualified, then proposesconfig.apply. - You do
- A different person approves, a human signs, then you promote.
- Read more
- ICP and scoring, Score engine
Tune how fast intent fades
Our sales cycle is <short or long>. Set a half-life of <N> days for web and topic intent, <N> days for buying triggers, and weight our own website and mailbox signals <X> times. Edit only those fields of the default scoring model. Show me the rescan preview before anything is signed.- Claude does
- Edits the decay fields in the default model, validates, and runs
score.rescan_previewafter the change is signed in dev. - You do
- Read the preview: moves into and out of qualified, tier churn, handoffs a real rescan would fire. Approve and sign.
- Read more
- Score engine, If scores do not move
Set routing and territories
Design territories. <Region and size rule> goes to <team A> (rotate between <names>). <Other rule> goes to <team B>. Our named accounts always go to their account owner (I will paste the domains). Show every territory with match rules, priority and rotation, then preview three real accounts with territory.match. Do not save anything. Tell me the exact values to enter in Engines > Routing.- Claude does
- Reads
routing.member_settings.list, drafts the territory values and previews real accounts withterritory.match. - You do
- Needs a human Save the territories in Engines > Routing.
- Read more
- Routing and territories
Build a sequence with exit rules
Write a <N>-step email sequence called <key>. Step 1 at once, then +<a>, +<b>, +<c> business days. Send only <window> in the recipient's time. A/B test two subject lines on step 1, 50/50. Steps use a brief, and claims must come from approved proof. Exit on reply, unsubscribe, bounce or a booked meeting. Bind it to lane <lane>. Propose it in dev. Do not enrol or send anything.- Claude does
- Proposes
definition.sequencewithsendWindow, two variants andexits, thensequence.createwith the step plan key. - You do
- Approve, sign, promote. Activate the sequence after you check the steps preview. Each real send needs a different person's approval.
- Read more
- Plays, sequences, lanes, Outreach engine
Add suppression and consent rules
We start prospecting in <countries>. Seed the standard compliance policies into dev. Add a suppression rule for our competitors and current customers (I will paste the domains). Then show how our email lane treats a contact in <country> with no recorded consent. Propose changes only, send nothing.- Claude does
- Runs
compliance.seed_policies, proposesdefinition.suppression_rulerules, readsconsent.getand the lane checks to explain the outcome. - You do
- Approve, sign, promote. Turn GDPR mode on in the console if you need it. Set guardrails in Settings > Safety.
- Read more
- Suppression and compliance, Rules every engine shares
Set claims, voice and proof
Our voice: <tone>. Avoid: <words>. Never claim: <claims>. I am attaching a case study. Extract draft proof and claims from it and show each claim next to the sentence it came from. Propose the brand voice in dev. Do not activate any proof. Do not draft any message.- Claude does
- Proposes
definition.brand_voice, runsproof.extract_from_documentto create a draft proof source and lists its claims. - You do
- Review the claims. Needs a human Proposing activation of proof is human only, and a different person approves it.
- Read more
- Claims, voice and proof
Define a KPI and check the money
Define a metric called <key>: <plain-words description>. Propose it in dev and run metric.evaluate on it. Then show where our revenue truth comes from, the attribution window and the FX convention, and reconcile revenue truth against the CRM's won deals for the last 6 months. Change nothing about money.- Claude does
- Proposes
definition.metric, evaluates it, then readsmoney.settings.get,money.reconcileandmoney.fx_drift. - You do
- Approve and sign the metric. Changing money settings is human only (Settings > Money).
- Read more
- Metrics and money
Data and connectors
Import and gate a messy CSV
I will paste a CSV of <companies, contacts or deals>. Map the columns to canon fields. Link each row to its company by website. Run a preview only. Show counts for create, update, held and error, the unmatched companies, and every row with a blank match value. Do not commit.- Claude does
- Runs
canon.importwithmode: "preview"and an explicitmapping. - You do
- Fix the mapping or the file. When the preview is right, commit in the console, or ask Claude to commit (its commit is a proposal a teammate approves). Then run the data quality pass.
- Read more
- CSV import and its rules, Data Quality engine, If an import is blocked
Connect a CRM and mirror it
Check the Composio connection and link every app I already connected there. Set <hubspot, salesforce, pipedrive, attio or zoho> as the CRM of record and run one sync. Then audit the mirror: source row counts against what GTM OS holds, anything truncated, anything below the flag threshold. Read only after the first sync.- Claude does
- Runs
composio.status,composio.sync_connections,crm.settings.set,crm.sync, thensync.healthandsync.completeness.run. - You do
- Connect Composio and the CRM app first.
crm.settings.setneeds an ACTIVE account for that CRM. - Read more
- Composio, CRM mirror and sync health, If the mirror drifts
Add a connector through Composio
Map <app and list tool> to <signals, companies or contacts> every <interval>, matched to contacts by <email>. Preview live data first with source.preview and show me the proposed field mapping before you propose it. Do not sync until the mapping is signed.- Claude does
- Runs
source.list_tools,source.preview,source.suggest_mapping, then proposes thedefinition.source_mapping. - You do
- Connect the app on the Composio card. A different person signs the mapping, and promotes it if prod should run it. Then ask Claude to run
source.sync. - Read more
- Source mappings, Composio
Add a connector through your own MCP server
Add our internal API as an MCP server named <name> at <https url ending in /mcp>. Test it and list its tools. Propose a binding that uses its <tool> tool for the capability <capability, for example enrich.company>. Tell me which tools I must mark Read.- Claude does
- Runs
mcp_servers.add,mcp_servers.test, then proposes adefinition.bindingwithbackend: "mcp". - You do
- Mark each lookup tool as Read (human only). Sign and promote the binding.
- Read more
- Your own MCP server, Build a connector
Price enrichment before you spend
Estimate the cost of enriching <N> companies missing <fields>. Show the expected and the worst-case cost, the providers it would use and the monthly cap. Do not run it. If I say go, run it with maxEntities <N> and maxUsd <amount> and show fill rate before and after.- Claude does
- Runs
enrichment.enrich_accountswithestimate: true,enrichment.budget.get, and after your go, a capped run. - You do
- Say go. Raising the monthly cap (
enrichment.policy.set) is human only. If the engine is dry, a run returns an estimate only. - Read more
- Enrichment engine, looot
Send events out to your own endpoint
Propose a webhook subscription to <https url> for the event types <gtm.reply.received, gtm.meeting.booked>. Generate a random secret of at least 16 characters, use it in the call, and print it to me once so I can set it in my receiver. Do not write it to any file. Then list the subscriptions.- Claude does
- Calls
webhooks.create(a proposal), thenwebhooks.list, which never shows the secret. - You do
- A second person approves. An owner or admin can stop deliveries later with
webhooks.disable. - Read more
- Webhooks, Event names
Go live and govern
Take one engine from dry to live
Show the safety state: Dry Mode, the stored and effective mode of every engine, the guardrails and today's usage, anything held. I want <engine> live with <caps and quiet hours>. Draft the exact policy.guardrails.set spec and the engine mode change. Tell me what I click and who approves what afterwards. Change nothing.- Claude does
- Reads
policy.get_dry_mode,policy.engine_modes.get,policy.guardrails.getand.usage. Drafts a complete spec (it replaces the whole spec). - You do
- Needs a human Save guardrails and switch the engine in Settings > Safety.
- Read more
- Dry Mode and live, Rules every engine shares, Guardrails
Create an agent with bounds
Design a read-mostly agent that <does one job on a schedule>. It may read records and create tasks only. Bounds: <N> steps, <amount> in cost, <N> minutes. Package it as a declarative pack named <scope/name> with its skill, agent and a schedule trigger. Validate and build it. Show what the approval will list. Do not install anything.- Claude does
- Scaffolds and validates the package, builds the artifact, and shows scopes, skill operations, bounds and trigger.
- You do
- Read the operations list line by line. Install and enable. A second admin approves.
- Read more
- Create an agent, Bounds, Packages
Prepare an envelope
I want <agent or principal> to be able to <operation pattern, for example enrich.*> without asking each time, up to <amount> EUR, from <date> to <date>. Draft the exact envelope.grant input. Tell me what the envelope does not cover and why a first message to a new contact still needs a person. Do not grant anything.- Claude does
- Drafts
envelope.grantinput (grantee, operation patterns,budgetEur, window). It cannot grant it. - You do
- Needs a human A person grants the envelope.
envelope.revokeends it at once. - Read more
- Envelopes, The autonomy ladder
Promote dev to staging to prod
Everything I approved is in dev. Check which versions are signed and who may sign the rest. Propose the unsigned ones for signing. Then preview env.promote from dev to prod and list everything it would carry, including things I did not mean to ship. Stop before you propose the promotion.- Claude does
- Reads
config.promotion_status, callsdefinition.propose_for_signing, thenenv.promotewithpreview: true. - You do
- A different human signs (Environments > Go live). Then ask Claude to propose the promotion. A different human approves it.
- Read more
- Life of a definition, Environments, Publish path errors
Review an approval
List every approval in state proposed. For each one say what it will do, who proposed it, which engine owns it, how long until it expires and who may approve it. Flag any I cannot approve myself and why. Do not try to approve anything.- Claude does
- Reads
gtm_approvalsand each approval's attribution. - You do
- Needs a human Approve what you may in Approvals. Send the rest to the right person.
- Read more
- Approvals, Who approves what, If an approval is stuck
Build, operate and upgrade
Build a Mission Control tile
Add a Mission Control tile "<name>": a metric that counts <what>. Show me the metric definition and the changed tile list first. Propose both together in dev, then run metric.evaluate on the key to prove it returns a value.- Claude does
- Drafts
definition.metric, reads the current app withconfig.get, proposes one ChangeSet with the metric and the edited app. - You do
- A different person approves and signs. Promote to prod.
- Read more
- Mission Control tiles, Metrics
Add a screen
Build a table app of <object, for example open deals by stage with value and owner> and a queue of <object> that need review. Scaffold it from the object types, validate it and propose it. Tell me which operations each view calls.- Claude does
- Scaffolds the spec, wraps it as a
definition.app, thenconfig.propose_changeset,config.validate_changeset,config.apply. - You do
- Approve, sign, promote. Open it under Apps > All apps.
- Read more
- Declarative apps, Studio
Upgrade safely
In the instance folder, run gtm upgrade --check, then gtm upgrade --channel stable --dry-run. Tell me which version I run, which version stable points at, whether my CLI is new enough and what the upgrade would do. Remind me to take a manual backup and to store GTMOS_MASTER_KEY apart from it. Do not run gtm upgrade with --yes. Wait for me to say go.- Claude does
- Runs the two read-only commands and summarizes them.
- You do
- Take a backup, then say go (or run the upgrade yourself).
- Read more
- Update to a newer release, Backups, If an upgrade fails
Roll back a definition, or the whole release
The change to <kind and key> is too strict. Show me the version history in prod, show both specs, and propose config.rollback to the version before the last one. Then tell me how I would roll back the whole release instead, and what stays untouched.- Claude does
- Reads
config.history, shows both specs, proposesconfig.rollback. Explains the image swap for a release rollback. - You do
- A different human approves the rollback. A release rollback is a command you run yourself.
- Read more
- History and rollback, Roll back a release
Find out why nothing is being sent
Nothing is going out from outreach. Check in this order: engine mode, organization Dry Mode, guardrails and today's headroom, lane activation, mailbox, sequence status and signed step plan, claims gate. Report the first thing that blocks it and the exact fix.- Claude does
- Reads
policy.engine_modes.get,policy.guardrails.usage,lane.list,sequence.get,outreach.deliverability. - You do
- Fix the blocker. Going live and lane activation need a person.
- Read more
- Dry mode and sending checklist, Rules every engine shares
Trace one account end to end
Take the account <domain>. Show its fit and intent scores with reasons, the signals behind them, whether it was handed off, where its contacts sit in any sequence, and where its employee count came from. Read only.Habits that make every recipe safer
- Start with
Work in devand end withDo not promote anythinguntil you have read the diff. - Ask for the preview first:
score.rescan_preview,play.simulate,env.promotewithpreview: true,canon.importin preview mode,enrichment.enrich_accountswithestimate: true. - Say what Claude must not do. "Do not send, enrol, merge or spend" costs nothing and removes a class of surprises.
- Never paste a provider key or a secret into chat. Put it in Integrations > Connections.
- Ask Claude to name the environment it used. A change in
devis not live.
Troubleshooting by symptom
Find what Claude or the console showed you. Each row gives the cause and the fix. Messages are quoted from the 0.2.5 code, so you can search for them.
{ error: { code, message, remediation } }. Read remediation first. It usually names the exact fix. If Claude says something failed and does not show the code, ask: Show me the exact error code, message and remediation.Read the error code
Goal: turn a code into a next step.
| Symptom | Cause | Fix |
|---|---|---|
VALIDATION_FAILED (400) | The input does not match the schema (see details.issues), or a named thing does not exist. | Run gtm_describe <operation> and compare your input with the schema. |
UNAUTHORIZED (401) | No valid session, key or agent token. | New key, re-export GTMOS_AGENT_TOKEN, restart. MCP connection |
AUTH_EXPIRED (401) | A connection's sign-in expired (Composio, Google, Slack). Not your agent key. | Reconnect that connection in Integrations > Connections. |
BUDGET_EXCEEDED (402) | Spend would exceed a ledger budget. | A person raises the budget on purpose, or the call becomes an approval proposal. |
FORBIDDEN (403) | A role, a scope, a human-only rule, four-eyes or a governance check refused it. | Auth and scopes |
POLICY_DENIED (403) | The organization's active gate policy denies the operation. | Read it with policy.get_gate_policy. A person changes the policy. |
NOT_FOUND (404) | That id or operation does not exist in this organization (or this environment). | Check the id and the environment. |
CONFLICT, NOT_CONFIGURED, CAPABILITY_NOT_CONFIGURED (409) | The thing is in the wrong state, or a provider or connection is missing. | Run gtm doctor and fix the line it names. Doctor lines |
APPROVAL_REQUIRED (202) | Not an error. The action waits for a person. | Approvals |
RATE_LIMITED (429) | Too many calls in a minute. | Wait, then retry with fewer calls. The limits are set in the instance .env (GTMOS_RATE_LIMIT_API_PER_MIN, GTMOS_RATE_LIMIT_API_ANON_PER_MIN, GTMOS_RATE_LIMIT_API_IP_PER_MIN). Restart the API after a change. |
UPSTREAM_UNAVAILABLE, PROVIDER_ERROR (502), TIMEOUT (504) | The provider failed or timed out. | Retry later. Check the provider's own status page. |
When nothing matches
- Run
gtm_describe <operation>and compare your input with the schema. - For background work, read
runs.listandruns.get {runId}, or rungtm logsandgtm trace <runId>. - For anything that went through the gate, read
runtime.get_approval {approvalId}. - Report the exact code, message and remediation. Do not guess a workaround.
MCP connection
Goal: get the gtmos server connected with all nine tools.
| Symptom | Cause | Fix |
|---|---|---|
gtmos is not listed by /mcp | Claude Code was started before gtm init wrote .mcp.json, or you are in another folder. | Quit Claude Code and start it again in the workspace folder. Connect Claude Code |
| "No commands match /setup-gtmos" | Claude Code was already running when gtm init wrote the commands. | Quit and start it again in that folder. |
| Tools fail after you restart or upgrade the stack | The MCP session is stale. | Run /mcp to reconnect. |
UNAUTHORIZED "No valid session or agent token (send Authorization: Bearer ...)" | GTMOS_AGENT_TOKEN is missing, wrong, revoked or expired. Agent keys last 90 days by default. | Ask a person for a new key (Settings > Developer or gtm token create), set the variable, restart the client. Never paste the key into chat. |
| HTTP 403 on every call | The agent was killed, or the key belongs to another organization. The message reads "Agent "x" is killed -- its API keys are refused". | A person revives the agent in Settings > Agents (agent.revive), or issues a key for the right organization. |
gtm init says the API is not reachable and hints at an HTML page | You passed the console address. | Use the API address (default http://127.0.0.1:3001). |
gtm init exits with code 3, "REFUSED" | You gave it a person's credential. | Use an agent key. |
| Connection refused on port 3004 | The instance is not running, or the MCP port was changed. | Run docker compose ps in the instance folder. Check http://127.0.0.1:3004/health answers {"status":"ok", ...}. |
| Console sign-in sits on "Signing in..." with no error | The console was opened at http://127.0.0.1:3000. The sign-in cookie is set for localhost. | Open http://localhost:3000 (or your own console port). |
| Claude Desktop does not list gtmos | The bridge config is not valid JSON, Desktop was not fully quit, or the key is wrong. | Check the JSON, quit Desktop completely, read mcp.log. Connect Claude Desktop (this path is untested). |
Check the key outside Claude, from the instance machine:
curl -s -X POST "http://localhost:3004/mcp" \
-H "Authorization: Bearer $GTMOS_AGENT_TOKEN" \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Auth and scopes
Goal: know whether a refusal means "get a person" or "get a wider key".
| Symptom | Cause | Fix |
|---|---|---|
FORBIDDEN "Operation "x" can only be performed by a human" | A human-only operation: approve, execute, sign, key management, Dry Mode, and the others in the human-only list. | Hand it to a person with the operation name and the ids. |
FORBIDDEN "Operation "x" requires the "build" scope, which role "member" does not hold" | Your scope is limited by the current role of the person who made the key. | A person with the right role creates the key, or the role is raised. |
FORBIDDEN "Agent does not hold "engine.configure" in its own intersected scope ..." | The key was made without that scope. | New key with the Full GTM engineer preset (gtm token create --preset full-gtm-engineer). Scopes and presets |
FORBIDDEN "Only an owner or admin (or an agent acting for one) can ...; this agent acts for a "member"" | The key was created by a member. | A key created by an owner or admin, or the owner or admin does it in the console. |
[gtm token] GTMOS_TOKEN holds an API key. Keys cannot mint keys | You ran gtm token create with a key. | Run gtm login and use the session token. |
| "GTMOS_TOKEN is not set" | No session in this shell. | Run gtm login --email <owner email> and paste the export line. |
| "action.approve can never be granted to a key" | Expected. A person approves, never a key. | Approve in the console. |
A read-only key cannot read env.list | Some reads need the build scope. | Use the Full GTM engineer preset, or ask a person to read it. |
Approvals stuck
Goal: find out why nothing is approvable or why an approved action did nothing.
| Symptom | Cause | Fix |
|---|---|---|
| "An action cannot be approved by the identity that proposed it" | Self-approval. | A different owner or admin approves. |
| "An action proposed by your own agent must be approved by a different person" | The person whose key proposed it tried to approve. | A second owner or admin approves. If there is none, add one: gtm member add --role admin. Approvals |
| "An agent principal cannot approve or reject an action approval -- only a human can" | Claude tried to approve. | Never. A person approves in Approvals. |
Role "member" does not hold action.approve | Only owners and admins approve. | Ask an owner or admin. |
CONFLICT "ActionApproval has expired" | Proposals expire after 7 days. | Propose again. |
CONFLICT "ActionApproval is "approved", not "proposed"" | Already decided. | Read it with runtime.get_approval. |
CONFLICT "Action payload no longer matches the approved payload hash" | The action changed after approval. | Propose again. |
FORBIDDEN "An action cannot be executed by the identity that proposed it" | Four-eyes also applies to execution. | The approver, or another member, executes it. |
FORBIDDEN "Lane "x" is paused -- this approved action will not execute until the lane is resumed" | A monitor paused the lane after a failure spike. | Investigate, then a human runs monitors.resume_lane. |
| The queue is empty but you expected a send to wait there | Dry Mode or the engine's dry mode turns the action into a draft, not an approval. | Check policy.engine_modes.get. Dry mode checklist |
| You are the only person on the instance | Four-eyes needs a second person from day one. | Add one without email: gtm member add --email <e> --password <p> --role admin. Roles |
Dry mode and sending checklist
Goal: find the first thing that stops a send, in the order the system checks it.
Ask Claude: Nothing is going out. Check each line of this list in order and report the first one that blocks it.
| Symptom | Cause | Fix |
|---|---|---|
| 1. Organization Dry Mode is on | Effect operations become drafts. A banner shows in the console. | An owner or admin turns it off in Settings > Safety (policy.set_dry_mode, human only). |
| 2. The engine's own mode is dry | A new organization starts with every engine dry. The stricter of engine and organization mode wins. | Switch the engine live in Settings > Safety (policy.engine_modes.set, human only). Dry Mode |
| 3. A guardrail holds it | A reached cap or quiet hours hold the action. It runs later by itself. | Read policy.guardrails.usage and policy.trust_holds.list. Guardrails |
4. failedCheck: "lane_required" | No activated lane is bound to the send. The message reads "refusing an un-laned email send: no sending lane is bound to message ...". | Pass {laneKey, environmentId} or set the lane on the sequence. Activate it with gtm lane activate <laneKey> --env <envId>; a different person approves. |
5. mailbox_required | "email sends require a bound mailbox so warm-up and per-mailbox daily caps can be enforced". | Connect a Gmail or Microsoft 365 mailbox in Settings > Connected accounts and bind it. |
6. mailbox_daily_cap | The mailbox reached its daily cap. | Wait, or add mailboxes. Do not raise caps to dodge warm-up. |
| 7. Lane envelope budget exhausted | "lane "x" envelope budget exhausted ... re-approval required". | A person re-approves by activating again, or raises envelopeBudgetEur in the lane. |
| 8. Lane activation refused in prod with a sending domain | SPF, DKIM or DMARC do not verify. | Fix the DNS records, then activate again. |
| 9. Net-new rule | A first message or invite to a contact nobody has written to never auto-sends. | A person approves it. This is fixed. |
| 10. Claims gate | A stat, result or named customer has no active proof. | Load and activate proof in Settings > Proof library. Claims and proof |
| 11. Sequence not active, or its step plan is unsigned | The sequence waits for a signed plan. | Sign and promote the definition.sequence, then activate the sequence. |
| 12. Demo mode: "Sent" but nothing arrived | GTMOS_LIVE is not true. The fixture sender answered. | Set GTMOS_LIVE=true in .env, restart the API and worker. In live mode the fixture sender refuses. |
Scores not moving
Goal: find out why a change to scoring does not show on an account.
| Symptom | Cause | Fix |
|---|---|---|
| "My model is not used" (no error) | It is unsigned, still in dev, or you checked another environment. Engines read only the signed version in the call's environment. | Read config.list per environment, sign, promote, and name the environment on every call. Life of a definition |
The policy version reads score.default.v2 or score.icp_fit.default.v1 | No signed scoring model in that environment, so the built-in default ran. | Sign and promote definition.scoring_model with key default (and icp_fit). The version should read definition.scoring_model:default:vN. |
| A new signal kind scores nothing | Score counts only kinds that a rule names. | Add a rule for the kind. |
| Fit is blank | The account has none of the four fit fields (for example no employee count). Fit shows as unavailable, never as 0. | Enrich first. Enrichment |
| Regional fit disappeared | The built-in fit model is size only. | Sign your own icp_fit model. |
| Third-party signals do not count | A new workspace starts on strict verification. | Verify the signals, or set the gate. Signals |
| Everything moved after an upgrade or a model change | The first rescore after a fingerprint change re-scores accounts once. | Preview it first with score.rescan_preview. |
| Learning changed my thresholds | It never does. It moves rule and segment weights only, at most 30 percent per run, and only with at least 30 accounts with and 30 without a criterion. | Read score.learning_report. |
VALIDATION_FAILED on a model with both bounce and hardBounce | One bounce would count twice. | List only one of them. |
Imports blocked
Goal: get a CSV committed, or know why rows were held.
| Symptom | Cause | Fix |
|---|---|---|
FORBIDDEN "canon.import is a bulk write" | Members cannot commit by default. | Ask an owner or admin, or an admin turns on crm.settings.set {memberBulkWrites: true}. |
| Claude's commit shows as a proposal | An agent can preview but its commit becomes a proposal. | A different person approves it in Approvals. |
| Rows stay unlinked from a company | The company column matches no account by website, domain or name. An import never creates an account. | Create or import the companies first, then import the contacts. |
| Rows are "Held for review" | A new company or contact looks like an existing record, or like an earlier row of the same file. | A person chooses New record or Same record under Needs a human. |
| The same rows are created again on every re-run | A blank match value (domain, primary email or name) turns matching off for that row. | Fill the match column. The preview warns on those rows. |
| More than 1,000 rows | The limit is 1,000 rows per call. | Split the file, or let the console chunk it. |
| A blank cell did not clear a value | A blank cell never overwrites a stored value. | Edit the record, or use canon.edit. |
| A row with no name is an error | A new contact needs a full name, or first and last name columns. | Map the name columns. |
"no signed version" on source.sync | The source mapping is unsigned. | gtm definition sign definition.source_mapping/<key> --env dev (a person). |
CSV import and its rules lists every rule in one table.
Sync drift and connector problems
Goal: know whether the mirror is complete, and fix a broken connection.
| Symptom | Cause | Fix |
|---|---|---|
| A sync says truncated | A run reads up to 10,000 records per object. | Run again, or narrow the scope. Read crm.sync_status. |
| An object is flagged below the threshold | Source row count against GTM OS count is under the flag threshold (default 60 percent). | Run sync.completeness.run. An owner or admin tunes it with sync.completeness.settings.set. CRM mirror |
| Parity differences on amount, stage, status, email, name or domain | A sampled record differs from live. | Run sync.parity_check and read sync.parity_results. |
| A stage value is held for review | An unmapped value is held, never defaulted. | Run stage_map.unmapped and stage_map.suggest. A person sets the mapping. |
crm.settings.set refused | No ACTIVE Composio account for that CRM. | Connect the CRM app on the Composio card first. |
NOT_CONFIGURED "Composio is not configured for this organization. Add your Composio API key in Integrations > Connections > Composio ..." | No Composio credential. | Integrations > Connections > Composio > Connect Composio, or a key under Advanced. |
VALIDATION_FAILED "This is a Composio user API key (uak_...) ... Nothing was saved." | A uak_ key was pasted. | Use Connect Composio, a Connect key ck_..., or a project key ak_.... |
AUTH_EXPIRED "Composio sign-in expired or was revoked -- Reconnect Composio ..." | The OAuth refresh failed. | Click Reconnect Composio on the card. |
| "Composio: hubspot is not connected for this organization ..." | That toolkit has no ACTIVE account. | Connect it on the Composio card. Check composio.status. |
An account is INITIATED, EXPIRED or FAILED | Hosted auth was not completed. Links expire after about 10 minutes. | Reconnect from the card, then composio.refresh. |
NOT_CONFIGURED "MCP server "x" is not registered for this organization" | The server is not added, or the name in the binding is wrong. | mcp_servers.add it (owner or admin), or fix the name. |
FORBIDDEN "no declared effect" on an MCP tool | The tool is not marked Read or Write. | A person marks it (mcp_servers.set_tool_effects). |
| "initialize failed ... did not answer with a JSON-RPC result" | The URL is not a streamable HTTP MCP endpoint, often missing /mcp. | Fix the URL. |
402 insufficient_balance from looot | The looot balance is empty. | A person tops it up. Do not retry. |
APPROVAL_REQUIRED on a lookup | The estimate is above LOOOT_MAX_COST_USD, the binding's maxUnitCostUsd or a budget. | A person approves, or raises the cap on purpose. looot |
Publish path errors
Goal: move a definition live without fighting the gate.
| Symptom | Cause | Fix |
|---|---|---|
FORBIDDEN "Definitions cannot be published directly into a prod-class environment ("prod") ..." | You applied a definition straight to prod. | Apply to dev, sign, promote. Life of a definition |
FORBIDDEN "Cannot promote "definition.lane/primary-email" to prod-class environment "prod" -- the source version (v2) is "draft", not "signed"" | The source version is not signed. | gtm definition sign definition.lane/primary-email --env dev (a person), then promote. |
| "definition.sign is human-only -- an agent principal can never sign a definition" | Claude tried to sign. | A person runs gtm definition sign. |
FORBIDDEN "Cannot sign: you proposed this version for signing -- a different person must sign it (four-eyes)" | You, or your agent, proposed it. | Another owner or admin signs: Environments > Go live > Review & sign all proposed. |
VALIDATION_FAILED "Illegal promotion prod -> dev" | A backward or same-stage promotion. | Promote upward: gtm promote --all dev prod. |
CONFLICT "ChangeSet ... is stale: N resource(s) got a newer version after it was proposed" | A newer version landed after the ChangeSet was proposed. | Re-propose from the current state. Use overwrite: true only to replace it on purpose. |
VALIDATION_FAILED "Unknown environment "stage" for this organization" | Typo, or the environment was not created. | Run env.list. Omit the environment for prod. |
| Banner "N definitions are in dev, not live" | Signed or proposed config sits in dev. | Environments > Go live: review and sign, then promote. A different member approves. |
gtm plan --from or gtm apply --from says "file not found" | The CLI flag takes a blueprint file, not a catalog id. | Pass the path to a blueprint file, or use blueprint.apply with a catalog id through Claude. |
| Search blueprint refused: "requires the GSC property" | No Search Console property was passed. | Pass gscProperty to blueprint.apply or --gsc-property sc-domain:example.com to gtm apply. |
Doctor and upgrade failures
Goal: read gtm doctor like a checklist, and recover from a failed upgrade.
Run gtm doctor in the instance folder (it needs GTMOS_API_URL, GTMOS_TOKEN and GTMOS_ORG). Each line starts with a tag.
| Line or message | Meaning | Fix |
|---|---|---|
LIVE | The provider answered. | Nothing. |
UNVERIF | Configured, not yet verified. | Run the connection's test (connections.test). |
FAILED | The credential was rejected. A !! line shows lastTestError. | New credential in Integrations > Connections. |
FIXTURE | Demo mode: fake deterministic data is served. | Never present it as real. Set GTMOS_LIVE=true and configure the provider. |
MISSING | Live mode, not configured. Using it fails NOT_CONFIGURED. | Follow the -> line under it: the exact env var or Connection to set. |
OPTIONAL | A seam this organization does not need. | Nothing. --strict ignores it. |
ERROR connector.gsc, exit 1 | SEO & GEO is in use and Google Search Console is not connected. | Connect it, set the property, run gtm doctor again. SEO & GEO |
gtm doctor --strict exits 1 | Something needed is not live, or an environment misses its default content. | Run the gtm pkg enable <package> --env <env> command it prints. |
Harness: WARN "initialised with CLI X, running Y" | The workspace files are older than the CLI. | Run gtm init --refresh --yes. |
Harness: WARN "skills X installed, this CLI pins Y" | Same cause. | Run gtm init --refresh --yes. |
Harness: WARN "reference generated from operations X, the live server has Y" | The server changed after you generated the reference. | Run gtm init --refresh --yes. |
Harness: INFO "unstamped" | The workspace was made before 0.2.5. Informational. | Run gtm init --refresh once. |
[gtm doctor] <code>: <message> and exit 1 | The readiness call itself failed. | Fix the code it prints, usually UNAUTHORIZED. MCP connection |
Starting and upgrading
| Symptom | Cause | Fix |
|---|---|---|
| [gtm up] docker compose exited N | Common causes: GTMOS_REGISTRY is still the placeholder, or the images for that version are not published yet. | Check the output above the line. Set the registry. Check the version. |
| [gtm up] a host port is taken | One of the four host ports is in use. The message names the .env variable (or flag) to change. | Change that port, run gtm up again. |
| [gtm up] the containers started, but the API did not report healthy in time | The API is slow or crashed. | Run docker compose logs api migrate. Health checks |
| [gtm setup] the API did not become healthy in time | Same. | Run the docker compose -f ... logs api migrate command it prints. |
| [gtm setup] AUTH_DATABASE_URL is missing from .env | The .env is incomplete. | Run gtm up --force in a fresh directory, or set it. |
| [gtm setup] needs --org <name> and --email <owner-email> | Missing arguments. | Pass them, or set GTMOS_ORGANIZATION_NAME and GTMOS_OWNER_EMAIL in .env. |
| The API, worker or MCP will not start | GTMOS_LIVE is not set to true or false. | Set it, then gtm up. |
| The upgrade refused before changing anything | A check failed: signature, path rule, CLI too old, disk, or a migration that is not additive. | Read the message. It names the check. Nothing was changed. |
| The upgrade failed part way | The images were swapped back and the backup is in backups/. | Run gtm doctor and the health calls. Rollback |
| You need data from a backup | A restore is a manual last resort. | Follow the restore steps. Rename the live database first. |
Reference
Lookup tables for release 0.2.5. Counts and names come from the 0.2.5 operation catalog. Your project holds the full generated list in docs/gtmos/reference/ after gtm init. If this page and your live instance disagree, the instance wins.
The MCP tools
Goal: know the nine tools Claude sees and which one to use.
| Tool | Use it to | Main inputs |
|---|---|---|
gtm_describe | List the catalog, or read one operation in full with its input and output schema. Call it first. | name, class (Q, D, T, E or M) |
gtm_query | Run a Q operation (a read). It rejects write operations. | name, input, environment |
gtm_act | Run a D, T, E or M operation. An E operation is proposed for approval, never executed directly. | name, input, environment |
gtm_get | Read one record of any object type by id. | objectType, id, environment |
gtm_search | List records of one object type, newest first, at most 50. | objectType, limit (1 to 50), environment |
search_records | Natural-language semantic search across types. Text-only when no embedding provider is set. | a query and filters |
gtm_approvals | Read the human-approval queue: list, read one, or count per engine. | id, engine, state (for example proposed), limit (1 to 500), count |
gtm_lineage | Read the receipts behind a record's values. | a record reference |
gtm_lineage_trace | Trace one value back to its source and the rule that picked it. | a record and a property |
Errors come back as { error: { code, message, remediation } }. See the error codes. Where environment is left out, the call runs in the project default (prod unless gtm init --env set another). Chapter 3 shows a worked call for each tool.
Operation classes and counts
Goal: know what a class letter means and how many operations there are.
| Class | Meaning | Count | Tool | Approval |
|---|---|---|---|---|
| Q | Query, a read | 294 | gtm_query | None |
| D | Data write | 255 | gtm_act | Runs at once. 12 of them propose instead. |
| T | Stage transition | 5 | gtm_act | Runs at once (see list below). 1 proposes. |
| E | Customer-facing effect | 30 | gtm_act | Proposed. A different person approves and executes. |
| M | Meta: config, ontology, platform | 87 | gtm_act | 21 of them propose. |
| Total | 671 | 66 propose for approval, 6 are gated (discovery only), 85 refuse agent keys. |
Required scope tier shown in the generated reference: operate for 593 operations (record.read for reads, record.write for writes), build for 57 (engine.configure, and an owner, admin or builder behind the key), admin for 15 (agents never hold it), and 4 package.publish and 2 package.entitle operations for the platform plane.
T operations: consent.withdraw, deal.advance_stage, flow.cancel, lane.activate, play.trigger.
E operations: actions.book_meeting, actions.cancel_meeting, actions.send_batch, capability.acquire, capability.submit_async, channel.deliver, connections.preflight, connections.test, crm.undo_update_record, crm.undo_write_outcome, crm.update_record, crm.write_outcome, enrichment.overwrite_field, enrichment.undo_overwrite_field, identity_dq.merge_records, identity_dq.revise_merge, identity_dq.unmerge_records, model.call, outreach.send_message, perf_ads.undo_campaign_change, perf_ads.update_campaign, runtime.execute_action, runtime.execute_approval, runtime.propose_action, seo_geo.publish_content_change, seo_geo.publish_to_cms, seo_geo.undo_content_change, seo_geo.unpublish_from_cms, source.undo_update_record, source.update_record.
Operations by area
The part before the first dot is the area. 109 areas, 671 operations. Counts are in brackets.
seo_geo (44), onboarding (26), agent (23), policy (17), score (16), identity_dq (15), outreach (15), enrichment (14), signals (14), actions (13), chat (12), composio (12), adapter (11), asks (11), config (11), connections (11), mission (11), packages (11), perf_ads (11), pipeline (10), pkg (10), research (10), runtime (10), sequence (10), crm (9), definition (9), field (9), money (9), records (9), source (9), journey (8), proof (8), query (8), slack (8), studio (8), canon (7), instance (7), replies (7), sync (7), tracking (7), connected_account (6), plays (6), transcripts (6), daily_update (5), mcp_servers (5), notification (5), privacy (5), resolve (5), routing (5), stage_map (5), activity (4), blueprint (4), capability (4), consent (4), deal (4), decision (4), delegation (4), env (4), envelope (4), flags (4), fx (4), monitors (4), org (4), play (4), roles (4), task (4), view (4), voice (4), apikey (3), brand (3), ontology (3), sso (3), webhooks (3), account (2), agency (2), apps (2), auth (2), binding (2), buying_roles (2), customer_type (2), deal_type (2), lane (2), ledger (2), metric (2), outbound_rails (2), reply_labels (2), retention (2), runs (2), search (2), secret (2), territory (2), audit (1), autolog (1), broker (1), channel (1), compliance (1), contact (1), data (1), email (1), flow (1), gtm (1), hooks (1), lineage (1), meeting (1), model (1), observe (1), product_analytics (1), project (1), sources (1).
gtm_describe with a class or a name for the exact list on your instance.Operations that refuse an agent key
Goal: know in advance which requests Claude must hand to a person. These 85 operations list "agent keys refused" in the 0.2.5 reference.
actions.backfill, adapter.activate, adapter.resolve, adapter.update, agent.bump_version, agent.create_key, agent.kill, agent.list_keys, agent.register, agent.revive, apikey.create, apikey.list, apikey.revoke, apps.update, auth.allowlist.get, auth.allowlist.set, brand.set, broker.accept_high_risk, buying_roles.set, composio.connect, composio.disconnect, composio.oauth_callback, composio.oauth_disconnect, composio.oauth_start, connected_account.delete_synced_data, connected_account.disconnect, deal_type.set, definition.sign, definition.sign_many, delegation.approve, delegation.grant, enrichment.policy.set, envelope.grant, envelope.revoke, identity_dq.revise_merge, instance.update.cancel, instance.update.check, instance.update.request, instance.update.settings, instance.update.status, mcp_servers.set_tool_effects, mission.approve, money.rate_sheet.clear, money.rate_sheet.set, money.settings.set, notification.preferences.revoke_links, notification.preferences.set, onboarding.launch, ontology.publish_changeset, org.offboard, outreach.bounce_undo, outreach.linkedin_webhook_secret, play.binding.set_status, plays.settings.set, policy.claims.set, policy.engine_modes.set, policy.guardrails.set, policy.set_autonomy_scope, policy.set_dry_mode, policy.set_gate_policy, privacy.settings.set, proof.propose_activation, reply_labels.set, research.fact.decide, roles.assign, roles.upsert, routing.member_settings.set, runtime.acknowledge_claim, runtime.approve_action, runtime.execute_action, runtime.execute_approval, runtime.reject_action, score.learning_settings.set, secret.migrate_scheme, secret.rotate_org_key, seo_geo.approve_draft, sequence.message.resolve_uncertain, signals.settings.set, signals.verify, sso.create, sso.delete, sso.list, sync.completeness.settings.set, territory.upsert, voice.customer_language.approve.
Other operations are not refused to a key but still end in a proposal that a different person approves. Chapter 4 explains why.
Definition kinds
Goal: pick the right kind when you ask Claude to change business logic. 31 definition kinds and 4 other config kinds ship in 0.2.5.
| Kind | What it holds | Chapter |
|---|---|---|
definition.agent | An agent version's authority scopes, activatable skills and hard run bounds (steps, cost, deadline) | Chapter 11 |
definition.app | A declarative app: routes, audiences and pages composing view types over registry operations | Chapter 13 |
definition.binding | How a capability is acquired: backend (looot / composio / mcp / native), the provider op, price cap, verification, fallbacks | Chapter 10 |
definition.brand_kit | A strict token -> value override map that re-themes the console design system | Chapter 13 |
definition.brand_voice | Tone, do / don't rules and vocabulary for generated copy | Chapter 7 |
definition.committee_model | The buying committee a motion expects: required roles, champion role, recency window | Chapter 7 |
definition.compliance_policy | Regulatory / policy rules a message or action must satisfy (consent, regions, claims) | Chapter 7 |
definition.customer_type | One kind of customer in the org's own words (agency, reseller, mid-market SaaS...): a CEL criteria over the company, a priority (a company is the firs | Chapter 7 |
definition.data_monitor | A data-quality monitor: what to check, threshold, schedule | Chapter 9 |
definition.deal_taxonomy | The deal types in the org's own words (pilot, demo, expansion...) and the raw CRM values that mean each one | Chapter 7 |
definition.enrichment_policy | How long an enriched value stays fresh, per field, in days (ttlDays: { primaryEmail: 90, phone: 180, ... }; defaultTtlDays for fields nothing else nam | Chapter 9 |
definition.identity_rule | How records of a type are matched / deduplicated and which source wins (survivorship) | Chapter 7 |
definition.journey | A lifecycle a company, person or deal follows: ordered stages (open / won / lost), optional pipeline-stage and stage-machine links, the segments route | Chapter 7 |
definition.lane | An outbound channel lane: channel, daily cap, touch window, deliverability guardrails, envelope budget, sending domain; a LinkedIn lane also its seat | Chapter 7 |
definition.metric | A derived KPI over the ontology (aggregate + filter + grain), evaluated as parameterized SQL | Chapter 7 |
definition.model_pin | Pins an agent version + task class to an exact model for reproducibility | Chapter 11 |
definition.model_policy | Which provider + model serves each task class (a model swap = a config change) | Chapter 11 |
definition.money | The org's money definitions: where revenue truth lives (CRM deal amounts, or a warehouse / billing object), the attribution window, and the FX convent | Chapter 7 |
definition.persona | Who you target inside an account: a buyer persona as a CEL criteria over contact fields (title, seniority, ...) plus traits | Chapter 7 |
definition.play | An automation: trigger (event / schedule / segment) + filter + steps (operations) + gate / guardrails | Chapter 7 |
definition.predicate | A named, reusable CEL boolean expression over a record's fields | Chapter 7 |
definition.reply_labels | The labels a reply gets in the org's own words (interested, not now, objection, referral, unsubscribe, out of office, bounce...), the phrases / patter | Chapter 9 |
definition.scoring_model | How a record is scored: weighted rules over signal kinds / firmographics, with qualified + needs-review thresholds | Chapter 7 |
definition.segment | Which accounts (or other records) a motion works: an objectType + CEL criteria | Chapter 7 |
definition.sequence | A sequence's steps in the org's own words: ordered steps (email or LinkedIn | Chapter 7 |
definition.skill | A bounded authority grant (operations, unmask paths, cost ceiling) an agent activates one at a time | Chapter 11 |
definition.source_mapping | How one list tool of a connected app (Composio, MCP) maps into the ontology: target object, field mappings + transforms, match keys, schedule, write-b | Chapter 10 |
definition.stage_machine | The legal states of an object and its guarded transitions (e.g | Chapter 7 |
definition.stage_map | What each value of one source's lifecycle / stage field means: a journey stage or ignore (key <system>.<account\|contact\|deal>) | Chapter 7 |
definition.suppression_rule | Who must never be contacted (CEL criteria + reason) | Chapter 7 |
definition.unit_economics | The economics a motion runs on: gross margin, AOV, COGS, CAC targets, repeat rate, aMER curve | Chapter 9 |
ontology.field | A field on an object type (type, required, enum values, reference target, sensitivity) | Chapter 5 |
ontology.object | A tenant-custom object type (key, label, description) | Chapter 5 |
ontology.relationship | A typed relation between two object types (cardinality, inverse label, deletion rule) | Chapter 5 |
package.manifest | A GTM OS package manifest (gtmos.package.yaml): what it contributes and the permissions it needs | Chapter 13 |
The full field tables are in your project at docs/gtmos/reference/definition-kinds.md. Chapter 5 explains the lifecycle: draft, propose, sign, promote.
Engine keys and mode keys
Goal: use the right key in policy.engine_modes.set, gtm_approvals and autonomy scopes.
Nine trust keys are the engines whose actions go through Dry Mode, approvals and guardrails. Each can be dry or live. A new organization starts with every one dry. An organization from before 0.2.5 inherits the organization-wide Dry Mode until a person sets an engine.
| Key | Label in the console | Owns these operations |
|---|---|---|
outreach | Outreach and sequences | outreach.*, sequence.*, actions.send_batch |
meetings | Meeting booking | actions.book_meeting, actions.cancel_meeting |
perf_ads | Performance ads | perf_ads.* |
seo_geo | SEO and GEO publishing | seo_geo.* |
plays | Lifecycle plays | every action a play takes, on top of the engine that runs it |
routing | Routing notifications | channel.deliver |
crm_writeback | CRM write-back | crm.*, source.* |
enrichment | Enrichment spend | enrichment.* |
identity_dq | Duplicate merges | identity_dq.* |
Mode values: dry, live. Default mode values: dry (new organizations), inherit (older ones). Shadow metrics used by policy.shadow_report: engagement_after_draft, score_vs_deal, drafted_only.
The seven packaged engines are stdlib/engine.score, .outreach, .performance-ads, .search, .identity-dq, .enrichment and .signals. Chapter 8 maps each to its console page and mode key. Signals has no mode key.
Event names you can build on
Goal: start a play or an agent trigger on the right event.
These 15 events are the subscribable events in 0.2.5. A play (trigger kind event) or an agent event trigger can start on any of them. The right column lists the payload fields you can filter on.
| Event | When | Filter fields |
|---|---|---|
gtm.signal.observed | A buying signal is observed | signalId, kind, category, accountId, contactId, source |
gtm.signal.verification_changed | A signal's verification changes | signalId, kind, toStatus, released, accountId, contactId |
gtm.deal.stage_changed | A deal changes stage | recordId, fromState, toState |
gtm.journey.stage_entered | A record enters a journey stage | objectType, recordId, journeyKey, stageKey, category |
gtm.consent.changed | A contact's consent changes | contactId, channel, purpose, state |
gtm.meeting.booked | A meeting is booked | meetingId, title, startsAt, accountId, source, sourceCategory |
gtm.reply.received | A reply is received | replyId, channel, sourceSystem, contactId, accountId, dealId, campaignId, sourceCategory |
gtm.record.created | A record is created | objectType, recordId, source, sourceCategory |
gtm.transcript.ingested | A call transcript is ingested | transcriptId, sourceSystem, title, accountId, contactId, dealId, meetingId, sourceCategory |
gtm.mission.step | A mission step runs | missionId, stepKind, stepIndex, action, state, agentPrincipalId |
gtm.connection.created | A connection is created | connectionId, provider, environmentId |
gtm.connection.tested | A connection is tested | connectionId, provider, result |
gtm.sync.completed | A sync finishes | monitorRunId, monitorKey, syncKind |
gtm.dry_draft.produced | Dry Mode held a new draft | ledgerEntryId, engine, actionType, reason |
gtm.scores.computed | An account is scored | accountId, qualificationId, verdict, fitAvailable |
Events from bulk writers (import, CRM sync, enrichment) carry sourceCategory: "bulk", and plays skip them unless they set includeBulkSources. Two more event names appear in the first-win checklist: gtm.definition.signed and gtm.onboarding.build_finished. They are not in the subscribable list, so do not build plays on them.
Scheduled plays can also aim at built-in audiences: deal.stage_stuck, deal.closed_lost, subscription.renewal_due, contact.champion_moved, meeting.ended and content.refresh_due. Those are audiences, not events.
Ledger action types
The engines also write audit rows you read in Operate > Actions and with actions.list: outreach.message_drafted, outreach.message_sent, perf_ads.campaign_changed, seo_geo.page_published, identity_dq.records_merged, enrichment.field_overwritten, crm.outcome_written. These are action types, not events a play can start on.
The gtm commands
Goal: find the command for a job. Run gtm help for every flag.
| Command | What it does |
|---|---|
gtm up | Write a pinned docker-compose.yml and .env into a folder and start the stack. Flags include --version, --channel, --dir, --dry-run, --no-start, --force, --bind and four port flags. |
gtm setup | First run in one go: up, wait for the API, bootstrap, onboard, add the approver, worker, harness. Needs --org and --email. |
gtm bootstrap, gtm onboard | Create the first operator and organization. onboard also seeds from a blueprint file. |
gtm member add | Add a verified teammate without email (--role admin|member). |
gtm init | Verify the instance and key, scaffold the Claude Code and Codex workspace, write docs/gtmos/. --refresh keeps it current. init secrets generates the self-host secrets. |
gtm harness | Scaffold only the agent workspace. Alias of gtm init --harness. |
gtm login, gtm token create|list|revoke | Sign in, and manage agent keys. Presets: full-gtm-engineer, read-write, read-only. |
gtm doctor | Readiness, one line per provider, plus the Search Console check, default content, packages and harness stamps. --status, --json, --strict. |
gtm plan, gtm apply, gtm check | Preview, propose and CI-check config. --from takes a blueprint file. apply also takes --param, --gsc-property, --breaking. |
gtm definition propose-for-signing|sign|sign-all | Move definitions through sign-off. Signing is human only. |
gtm promote, gtm rollback | Promote one resource or --all between environments. Roll a resource back with --to-version. |
gtm mirror export | Project the Config Store onto an optional git mirror. |
gtm blueprint | list, show, upstream, upgrade for the blueprint catalog. |
gtm lane activate | Propose activating a sending lane. A different person approves. |
gtm play simulate | Dry-run a play over the last N days, zero writes. |
gtm logs, gtm trace | List durable flow runs and show one run's step journal. |
gtm hooks list | Show the hooks runtime. |
gtm connect, gtm capabilities | Bind a source with a mode. Discover and inspect backend operations and prices for a capability. |
gtm pkg | scaffold, validate, build, test, sign, publish, registry list, provenance, lock, upgrade, and the gated install, enable, disable, uninstall. |
gtm app generate | Scaffold a definition.app from an object type. |
gtm describe, gtm codegen, gtm profile | Print the operation catalog, regenerate generated files, profile an object type. |
gtm upgrade, gtm version | Upgrade an installed instance (--check, --dry-run, --yes), and show current against available versions. |
gtm updater | install, uninstall, status, run for one-click updates from Settings > Updates. |
gtm org export | Export one organization's data as Parquet or SQL. |
gtm byoc | plan, apply, destroy for the client's own AWS account. |
gtm fleet | Managed fleet commands for operators. |
gtm enqueue-retention-sweep, -listener-dispatch, -webhook-dispatch | Internal: seed the first tick of a self-rescheduling job. |
Config and environment keys
Goal: find the key you must set, and know which ones need a restart.
Set keys in the instance .env, then run gtm up so the containers are recreated, and /mcp to reconnect. Provider keys you add as organization connections are live at once.
| Group | Keys |
|---|---|
| Release | GTMOS_REGISTRY, GTMOS_VERSION (a channel such as stable, or a pinned version) |
| Addresses | PUBLIC_API_URL, PUBLIC_CONSOLE_URL, PUBLIC_MCP_URL, GTMOS_BIND_ADDRESS (default 127.0.0.1), TRUST_PROXY |
| Mode | GTMOS_LIVE (true or false, required) |
| Secrets | BETTER_AUTH_SECRET, GTMOS_MASTER_KEY, CREDENTIAL_ENCRYPTION_KEY, AUDIT_CHECKPOINT_SIGNING_KEY, GTMOS_OAUTH_REGISTRATION_TOKEN |
| Database | POSTGRES_PASSWORD, DATABASE_URL, AUTH_DATABASE_URL |
| Client side (Claude) | GTMOS_API_URL (no /v1), GTMOS_TOKEN (session), GTMOS_AGENT_TOKEN (agent key), GTMOS_MCP_URL, GTMOS_ORG |
| First run | GTMOS_ORGANIZATION_NAME, GTMOS_OWNER_EMAIL, GTMOS_OWNER_PASSWORD, GTMOS_APPROVER_PASSWORD, GTMOS_INSTANCE_ADMIN_EMAILS |
| Sign-in and cookies | GTMOS_SIGNUP_OPEN, GTMOS_COOKIE_DOMAIN, GTMOS_CROSS_SITE_COOKIES, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET, MICROSOFT_TENANT_ID |
| Rate limits (per minute) | GTMOS_RATE_LIMIT_AUTH_PER_MIN, GTMOS_RATE_LIMIT_OAUTH_PER_MIN, GTMOS_RATE_LIMIT_API_PER_MIN (per session or key), GTMOS_RATE_LIMIT_API_ANON_PER_MIN (per IP, no credential), GTMOS_RATE_LIMIT_API_IP_PER_MIN, MCP_MAX_BODY_BYTES |
| Model providers | ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, OPENROUTER_API_KEY |
| Outside data and apps | LOOOT_TOKEN, LOOOT_MAX_COST_USD, COMPOSIO_API_KEY, COMPOSIO_WEBHOOK_SECRET, UNIPILE_API_KEY, UNIPILE_DSN, UNIPILE_WEBHOOK_SECRET, GSC_ACCESS_TOKEN, GSC_SITE_URL, GOOGLE_ADS_*, META_ADS_ACCESS_TOKEN |
| Sending | RESEND_API_KEY, RESEND_FROM_ADDRESS, RESEND_WEBHOOK_SECRET |
| System email | EMAIL_TRANSPORT (smtp, webhook, none), SMTP_HOST, SMTP_PORT, SMTP_SECURE, SMTP_USER, SMTP_PASS, EMAIL_FROM, RESET_PASSWORD_WEBHOOK_URL, VERIFICATION_EMAIL_WEBHOOK_URL, INVITATION_EMAIL_WEBHOOK_URL, OPERATOR_NUDGE_WEBHOOK_URL |
| Fallback to instance keys | GTMOS_PLATFORM_CREDENTIAL_PROVIDERS (replaces the default list when set) |
Workflow flags set with flags.set (or Settings > Feature flags and Settings > Workflow automation): qualification.handoff, signals.reaction, signals.scan, identity_dq.thresholds. Provider settings (not secret) go through connections.set_provider_setting, for example gsc/site_url.
Client files your project holds: .mcp.json (the gtmos server), .claude/gtmos-instance.json (harness stamps), .claude/gtm-context.md, CLAUDE.md, AGENTS.md, and docs/gtmos/.
Glossary
- Agent key
- The credential Claude uses (
GTMOS_AGENT_TOKEN). It acts as the agent, proposes, and can never approve. - Approval
- A proposed action waiting for a person other than the proposer. Proposals expire after 7 days.
- Autonomy ladder
- Five levels, A0 Observe to A4 Mission autonomy. The default is A2 Propose. It only removes one brake. It does not turn automation on by itself.
- Binding
- How a capability is acquired: backend (looot, Composio, MCP or native), the provider operation, a price cap, verification and fallbacks.
- Blueprint
- A ready-made set of definitions for a go-to-market motion. Ten ship in 0.2.5. Apply one in dev as a proposal, then customize.
- Canon
- The built-in set of object types (accounts, contacts, deals and others) and the rules for editing them.
- ChangeSet
- A bundle of definition changes proposed together, validated, approved by a different person, then published.
- Claims gate
- A check that a stat, result or named customer in outbound copy is backed by active proof.
- Composio
- The way GTM OS connects to your own apps (CRM, Slack, Search Console). looot is for outside data.
- Definition
- A signed, versioned piece of business logic. The engines read the signed version in the call's environment.
- Dry Mode
- While on, an effect becomes a draft and nothing leaves the system. There is an organization master and one mode per engine. The stricter wins.
- Effect (E)
- An operation that touches a customer or an outside system. It is proposed, never run directly.
- Engine
- One of seven packaged workers: Score, Outreach, Performance Ads, SEO & GEO, Data Quality, Enrichment, Signals.
- Envelope
- A scoped, budgeted, time-bound pre-approval that a person grants. It can approve matching operations without asking each time.
- Environment
dev,stagingorprod. Config lands in dev. Only signed definitions reach prod.- Four-eyes
- The person who proposes can never approve, sign or execute. A second person is needed from day one.
- Guardrails
- Organization-wide caps and the send window. A reached cap holds an action. It runs later.
- Human-only
- An operation that refuses an agent key. A person must do it in the console or the CLI.
- Lane
- A sending channel with its limits (daily cap, touch window, deliverability checks, envelope budget). Every send needs an active lane.
- Ledger
- The audit trail of engine actions (Operate > Actions).
- Lineage
- The receipts behind a value: which source, which rule.
- looot
- The data-acquisition layer for enrichment, emails, lists and intent.
- MCP
- Model Context Protocol. How Claude talks to GTM OS.
- Mission
- A bounded, approved plan that an agent steps through under limits on steps, cost and time.
- Net-new rule
- A first message or invite to a contact nobody has written to is never auto-sent.
- Operation
- One named capability on one surface (REST, SDK, CLI, MCP). 671 in 0.2.5, in five classes.
- Package
- A signed bundle that adds apps, definitions, agents or engines. Installing and enabling are proposals.
- Play
- An automation: a trigger, a filter, and steps that call operations.
- Promote
- Move signed config from one environment to the next. A different person approves.
- Proof library
- The approved evidence behind claims, in Settings > Proof library.
- Sequence
- An ordered set of email or LinkedIn steps with waits, variants and exit rules.
- Sign
- A person other than the proposer marks a definition version as the one engines may use.
- Skill
- A bounded authority grant (operations, cost ceiling) an agent activates one at a time. Also a Claude Code skill in your workspace.
- Trust key
- The name of an engine in the approval and mode system, for example
outreachorcrm_writeback.