GGTM OS Engineer guide Docs home
GTM OS engineer guide

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.

Written for GTM OS release 0.2.5 (CLI 0.2.5). Console paths are written as Group > Page; every page lives under /org/<orgId>/.

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 to prod.
  • 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.
Untested prompts. No prompt in this guide was run against a live instance while it was written. Operation names were checked against the 0.2.5 operation catalog. Run each prompt once on a dev instance in Dry Mode before you rely on it. Where a fact could not be confirmed, the page says so.

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.

Chapter 1

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.

  1. You say what you want in plain words.
  2. Claude looks up the exact operation (with the gtm_describe tool) and calls it with its own agent key. The key acts for you but is never you.
  3. 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.
  4. A different person with the owner or admin role approves the proposal in the console (Approvals), then executes it. Needs a human
  5. GTM OS writes down who proposed, who approved and what ran. You can read that trail in Settings > Audit log.
Claude's report names the operation, the environment it used, and an approval id when something is waiting. If it does not name them, ask for them.

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

PieceWhat it isWhere it comes up in this guide
OperationsThe 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
OntologyThe 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
EnginesThe packaged workers that score, enrich, listen, write, rank and publish. They read your definitions.Below
DefinitionsVersioned 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
GovernanceProposals, approvals, environments, agent identities, Dry Mode, the audit log.Chapter 4
Blueprints and packagesA 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.

ClassMeaningWhat happens when Claude calls it
QQuery, a readRuns at once. Nothing changes.
DData writeRuns at once. Changes records or settings inside your organization.
TStage transitionRuns at once. Moves a record to its next stage.
ECustomer-facing effectBecomes a proposal. Nothing is sent or written outside until a different person approves and executes it.
MMeta: configuration, ontology, platformDepends 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.

EngineWhat it doesWhere you see it
ScoreScores accounts from signals and firmographic fit, with the reasons behind each verdict.Engines > Score; CRM > Hot list
EnrichmentFills empty fields from providers, stamps freshness, re-checks stale values. Spend is real, so it always quotes first.Engines > Enrichment
SignalsIngests buying, growth and risk signals and weighs them by age.CRM > Signals; CRM > Hot list
OutreachDrafts, personalizes, sequences and sends. Every send is an approval-gated effect through an active lane.Engines > Outreach; CRM > Sequences
SEO & GEOAudits tracked pages, recommends changes, tracks rank and AI-search readiness, runs the content factory, publishes through an approval.Engines > SEO & GEO
Performance AdsSyncs spend, paces against your CAC target, proposes budget and status changes. This is real money.Engines > Performance Ads
Identity & data qualityFinds 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.

Only these seven are shipped. You may see a longer engine list in vision or roadmap documents. Treat any engine outside the table above as planned, not available.

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, staging and prod. prod is the default: a call that names no environment runs in prod. Definitions cannot be published straight into prod. They are written in dev, signed by a person, then promoted. Customer-facing effects in dev and staging are 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, dry or live, and in a new organization every engine starts dry until 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.

  1. Describe. Claude reads the operation or definition kind it is about to use.
  2. Change in dev. Claude writes the change to dev and says so.
  3. Propose. Claude reports the approval id and stops. Needs a human
  4. Approve and sign. A second person approves, then signs the definition. Needs a human
  5. Promote. One approval promotes everything from dev to prod (CRM > Environments, the Go live card). Needs a human
  6. 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 seeOpen
What is waiting for a decision, and its historyApprovals (top of the sidebar)
Only what is waiting on a person right nowOperate > Needs a human
What Claude wrote and who ran itSettings > Audit log
What is in dev and not yet live; sign and promoteCRM > Environments
Dry ModeSettings > Safety
Your agent keys and the connect snippetsSettings > Developer
Stop an agent, or bring it backSettings > Agents
Provider keys and connected appsIntegrations > Connections
Runs and their step-by-step tracesOperate > 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.
An answer that names your agent, the scopes (at least 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.

Untested. This prompt was not run against a live instance. Operation names were checked against the 0.2.5 catalog.

Chapter 2

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%\.wslconfig and restart Docker.
  • Node 22 or newer.
  • Claude Code.
  • The gtm CLI from npm:
npm i -g @gtmos/cli
docker info --format '{{.ServerVersion}}'
node -v
gtm version --offline
A Docker server version, v22.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.

  1. Make a folder and start Claude Code in it:
    mkdir my-gtm && cd my-gtm
    claude
  2. Inside Claude Code, the ! prefix runs a shell command. Run:
    ! gtm init
  3. Quit Claude Code and start it again in the same folder. Claude reads new slash commands and skills only at startup.
The output ends with "No GTM OS instance connected yet. Open Claude Code here and run /setup-gtmos" and "Restart Claude Code in this folder so it loads the new commands and skills". The folder now holds 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.

The key is never written to a file. .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-gtmos

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

PhaseClaude doesYou do
InstallRuns gtm setup ... --no-harness into ./instance. A few minutes. Safe to re-run.Nothing.
PasswordsTells 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 keySigns 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).
ConnectionsStores 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
After the restart, /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.

  1. Install into its own folder. The owner and approver passwords are generated into its .env:
    gtm setup gtm-instance --org "Acme" --email you@acme.example --approver-email cofounder@acme.example
    Port busy? Add --console-port, --api-port, --mcp-port or --postgres-port. Local images only? Add --version <tag> --no-pull.
  2. Sign in as the owner. It asks for the password, or reads GTMOS_PASSWORD. Your org id is ORGANIZATION_ID in gtm-instance/.env:
    export GTMOS_API_URL=http://127.0.0.1:3001 GTMOS_ORG=<org id>
    gtm login --email you@acme.example --console-url http://localhost:3000
    Run the export GTMOS_TOKEN=... line it prints. This is your owner session. It is not the agent key.
  3. Create the agent key. It is shown once:
    gtm token create --name claude-code --client claude-code --preset full-gtm-engineer --expires-days 90
  4. Put the key in your environment (next section).
  5. Connect the workspace and check it:
    gtm init --url http://127.0.0.1:3001 --key <agent key> --yes
    gtm doctor
  6. Start claude in 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}.

In a new terminal, 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.

  1. 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
  2. Set GTMOS_AGENT_TOKEN as above, then in the workspace folder run gtm init --url <api url> --key <agent key> --yes.
  3. 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"}'
A JSON answer that lists the nine tools. A 401 means the key is wrong, expired or revoked.

Keep the project current after you upgrade the CLI or the instance. Files you edited are kept:

gtm init --refresh --yes
Common 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 init wrote the commands. Quit and start it again in that folder.
  • MCP tools fail after you restart or upgrade the stack: run /mcp to reconnect.
  • gtm init says the API is not reachable and shows an HTML-page hint: you passed the console address. Use the API address (default http://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 init exits 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.

Claude Code is the recommended way to build. 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.

  1. Create a key for it in Settings > Developer. Choose Other MCP client, give it its own name (for example claude-desktop-anna), and pick the read-only preset unless you need more. Use a short expiry. Needs a human
  2. 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.json on Windows and ~/Library/Application Support/Claude/claude_desktop_config.json on macOS.
  3. Add the server. Node must be installed, because npx runs 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>" }
        }
      }
    }
  4. Quit Claude Desktop completely and start it again.
  5. In a new chat open the Add files, connectors, and more menu, then Connectors, and check that gtmos is listed.
The gtmos server appears with its tools. Ask "Call gtm_describe and tell me how many operations you can see." Claude answers with a number (671 in 0.2.5, fewer if your key cannot see some).

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.log on Windows, ~/Library/Logs/Claude/mcp.log on 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.whoami through gtm_query, runs gtm doctor in the shell.
You do
Fix what is not live, in Integrations > Connections.

Untested. This prompt was not run against a live instance. Operation names were checked against the 0.2.5 catalog.

Chapter 3

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

ToolUse it toChanges data?
gtm_describeList every operation, or read one operation's exact input and output shape. Always first.No
gtm_queryRun a Q-class operation (a read) by name.No
gtm_getRead one record by object type and id.No
gtm_searchList records of one object type, newest first, at most 50.No
search_recordsPlain-language search across record types, ranked by relevance. Returns short previews.No
gtm_actRun a D, T, E or M operation by name. An E operation becomes a proposal.Yes
gtm_approvalsRead the approval queue, count it, or read one approval.No
gtm_lineageRead the audit trail behind one approval: who proposed, who approved, what ran.No
gtm_lineage_traceRead 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 full

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

For 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_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.

For an E operation Claude reports "proposed", an approval id and who can approve. If Claude says "sent", that is wrong: ask it to read the approval back with gtm_approvals.

gtm_approvals and gtm_lineage: follow a proposal

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

Config written to 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.

CodeMeaning
VALIDATION_FAILEDThe input does not match the schema (see details.issues), the tool and class do not match, or a named thing does not exist.
UNAUTHORIZEDNo valid key. Missing, wrong, revoked or expired.
FORBIDDENA role, a scope, a human-only rule or four-eyes refused it.
NOT_FOUNDThat id or operation does not exist in this organization.
NOT_CONFIGURED, CONFLICTA provider or connection is missing, or the thing is in the wrong state.
APPROVAL_REQUIREDNot a failure. The action is waiting for a person.
RATE_LIMITEDToo 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_approvals and gtm_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.trace through gtm_lineage_trace.
You do
Nothing.
Chapter 4

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

ScopeLets the agent
record.readRead. Always included.
record.writeWrite records and propose customer-facing effects.
engine.configureChange configuration: definitions (as proposals), environments, settings, workflow flags, research settings. Called the build scope.
connection.manageStore 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.

PresetScopesDefault for
full-gtm-engineerAll fourClaude Code
read-writerecord.read, record.write
read-onlyrecord.read
(none given)record.read, record.write, engine.configureCodex 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.

SituationResult
The key has engine.configure, but a member (not an owner or admin) created itBuild operations refuse. The message says the agent acts for a member.
The person who made the key is demotedThe key shrinks with them at the next call.
The key is a read-only presetReads that need only record.read work. Writes and build reads (such as env.list) refuse.
Run 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.

  1. Console: Settings > Developer, Connect Claude Code / Codex. Choose the client, name, preset and expiry, then Create agent key. Copy the key now.
  2. 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
    --client is claude-code, codex or other. Use --preset full-gtm-engineer|read-write|read-only or --scopes, not both. --expires-days is 1 to 365 or never, and the default is 90.
  3. 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 login and 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.

WhyWhat
Four-eyesApprove, reject or execute an approval; sign a definition; publish an ontology change; approve a delegation or a mission; decide a research fact.
No self-escalationCreate 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 consentOAuth sign-ins (Composio, Google, Microsoft, Slack). Claude gives you the link.
Safety switchTurn Dry Mode on or off. Claude can read it.
IrreversibleOffboard 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

  1. The approver opens Approvals (or Operate > Needs a human) and clicks the item. Needs a human
  2. Read what it will do. For a config change, the impact preview is shown.
  3. Click Approve, then Execute.
The state moves from 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

  1. One key: Settings > Developer, revoke it, or gtm token revoke <key-id>. It stops working at once.
  2. The whole agent, everywhere: Settings > Agents, Kill. Every key of that agent is refused at every checkpoint. Needs a human
  3. Bring it back: Settings > Agents, revive. Only a person can.
A killed agent's calls fail with HTTP 403, and the message says the agent is killed.

Prompts for this chapter

Claude cannot mint a key. If you ask for "a key with build scope", Claude can only tell you the steps. You create the key.

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.whoami and maps each task to its class and scope with gtm_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

Untested. This prompt was not run against a live instance. Operation names were checked against the 0.2.5 catalog.

Chapter 5

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:

PartMeaning
kindWhat sort of logic it is, for example definition.scoring_model or definition.lane.
keyIts name inside that kind. Engines look up fixed keys, such as default and icp_fit for scoring. Keep those keys stable.
specThe settings. The schema is strict: an unknown field is rejected, never ignored.
Environment and versionEvery 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).

Ask Claude: "List every definition in dev and in prod and tell me which differ." It reads 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.

AreaKindsWhat they decide
Who to target and how they scoredefinition.scoring_model, definition.segment, definition.persona, definition.predicate, definition.committee_model, definition.customer_typeFit and intent scores, which accounts a motion works, who the buyer is, the buying committee, your own customer types.
Lifecycle and stagesdefinition.journey, definition.stage_machine, definition.stage_map, definition.deal_taxonomyThe 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 rulesdefinition.lane, definition.sequence, definition.reply_labels, definition.brand_voice, definition.suppression_rule, definition.compliance_policySending channels and caps, the steps of a cadence, how replies are labeled, tone and forbidden claims, who is never contacted, regulatory rules.
Plays, KPIs, moneydefinition.play, definition.data_monitor, definition.metric, definition.money, definition.unit_economicsAutomations, data-quality checks, derived KPIs, where revenue truth lives, the economics ads pace against.
Data indefinition.identity_rule, definition.source_mapping, definition.binding, definition.enrichment_policyHow 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.
Platformdefinition.agent, definition.skill, definition.app, definition.model_policy, definition.model_pin, definition.brand_kitAgent 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 with routing.member_settings.set (see Routing).
  • Workflow automation. The qualified-account handoff and the signal reaction are feature flags (qualification.handoff, signals.reaction), set with flags.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.

StepOperation (class)What it doesWho
1. Proposeconfig.propose_changeset (M)Takes envelopes[] and returns a changeSetId in state draft. Nothing is applied.Claude
2. Validateconfig.validate_changeset (Q)Checks every envelope against its kind's schema and returns {ok, issues[]}. Issues are feedback, not errors.Claude
3. Previewconfig.compute_impact (Q)Writes an impact preview the approver can read before deciding. Optional.Claude
4. Applyconfig.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.

Approvals in the console shows the proposal with the diff. gtm_approvals shows the same to Claude. The proposal expires after 7 days if nobody decides.
If it fails
  • VALIDATION_FAILED with an issue list: Claude fixes the spec and validates again. Typical causes are an unknown field, a missing required field, or a CEL variables map that does not match the expression.
  • CONFLICT on 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 an overwrite: true request, 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 happensOperationWhoState after
1You ask. Claude reads the current definition and the environment it will write to.config.get, config.listYou, Claudeunchanged
2Claude writes the full new spec and runs the change set steps (propose, validate, preview).config.propose_changeset, config.validate_changeset, config.compute_impactClaudeChangeSet draft
3Claude proposes the publish. An approver reads the diff in Approvals, approves and executes it.config.applyClaude, Needs a human (not you if you created the agent key)version vN in dev, status draft
4Claude asks for sign-off on that version.definition.propose_for_signingClaudestatus proposed
5A human signs. Not the person who proposed it, and not the person whose agent proposed it.definition.sign or definition.sign_manyNeeds a humanstatus signed, still in dev
6Claude proposes promotion of everything signed. One approval covers all of it, hop by hop (dev, then staging, then prod).env.promoteClaude, Needs a human approves and executeslive in prod
7Claude proves the engine now uses your version.score.score_account, config.promotion_statusClaudeverified

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 seeWhyFix
"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 nothingNothing signed differs from the target.Check with env.promote and preview: true.
Say it every time. Claude should always tell you: "I wrote X in dev. It is not live until it is signed and promoted." If Claude skips that line, ask for it.

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.

CommandWhat it does
gtm plan --env devPreviews 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 devProposes a change set. Still needs a different human's approval.
gtm definition propose-for-signing <kind>/<key> --env devdraft to proposed.
gtm definition sign <kind>/<key> --env devHuman 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 devHuman only. Signs every proposed definition in the environment, except the ones you proposed.
gtm promote --all dev prod --previewShows the plan: every resource, its version and diff, and what is skipped and why.
gtm promote --all dev prodProposes one approval for the lot.
gtm promote <kind> <key> dev prodPromotes one resource.
gtm rollback <kind> <key> --to-version 3 --env prodProposes a rollback (see below).
gtm checkCI 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 wantAsk Claude to use
Every version of one definition, newest firstconfig.history
A past version, or the state at a past momentconfig.get with a version number or an as-of time
Roll backconfig.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 environmentsconfig.drift, or CRM > Environments
Retire a version, with a sunset datedefinition.deprecate
Record who owns it, who signs it and whydefinition.set_governance (owner, approver, rationale), read with definition.get_governance
Find definitions that say different things about the same conceptdefinition.detect_contradictions
See which definitions are healthy or at riskdefinition.health
After a rollback is approved and executed, ask Claude to run 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.list for both environments, and config.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_model key default, proposes a change set with the full new spec, validates it and reports the impact preview, then proposes config.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_signing per version, then env.promote with preview: 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, proposes config.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.

Chapter 6

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 hereUseIt 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 finishedThe 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-systemEach engine built, verified live, with decisions written down.
You want to know if it is safe to switch on/go-live-checkOne 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 into instance/.env, and never prints them.
  • It mints its own agent key (preset Full GTM engineer), runs gtm init and gtm doctor, and tells you to set GTMOS_AGENT_TOKEN and 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.

After a restart, /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

ScreenYou doOperations Claude uses
1. WelcomeCompany profile, markets, motions, stack, pains, hours a week. Pick engines and a blueprint.onboarding.update, onboarding.suggest, blueprint.get
2. Connect your dataSee 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 definitionsUpload 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 whatBuying roles, consent, trust rules, autonomy per engine, seats and role profiles.onboarding.card.approve, onboarding.seats.set, roles.list
5. The buildCheck 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 daysShape 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:

CardPublished throughWho finishes it
Lifecycle stagesjourney.propose and journey.applyPending until a different human approves.
ICP and the weights sheetdefinition.scoring_model key icp_fit, by change setPending until a different human approves.
Pilot scopedefinition.segment, by change setPending until a different human approves.
Customer types and their journeysdefinition.customer_type, by change setPending until a different human approves.
Who the engines never contactdefinition.suppression_rule, by change setPending until a different human approves. Do-not-contact values are stored as keyed hashes, never plain text.
Your CRM stage valuesstage_map.setA human's call is final. An agent's call is a proposal.
Revenue truth and currencymoney.settings.setNeeds a human signed in.
Your words for dealsdeal_type.setNeeds a human
Who owns what (territories)territory.upsertNeeds a human
Brand voice and forbidden claimsbrand.setNeeds a human
Buying rolesbuying_roles.setNeeds a human
Consent and GDPRprivacy.settings.setNeeds a human
Dry mode and guardrailspolicy.set_dry_mode, policy.guardrails.setNeeds a human
Autonomy per enginepolicy.engine_modes.set, policy.set_autonomy_scopeNeeds 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.apply substitutes 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_adaptations previews 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.preview shows 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.
  • CONFLICT with LAST_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 --refresh in 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.

  1. 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.
  2. Recommend. Claude picks the closest general blueprint and says why. You confirm.
  3. Seed. Claude applies it in dev as a proposal.
  4. 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.
  5. 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:

CommandWhat it does
/define-objectDefine or change an object type (fields, interfaces, stage machine).
/create-playCreate and simulate a play.
/wire-connectorWire an external source or provider and set its binding mode.
/build-appScaffold a record page, queue, table or dashboard.
/onboard-blueprintSeed a whole motion from a blueprint.
/describe-opsList operations, or show one operation's schema.
/compile-ruleTurn one rule from the GTM playbook into a signed definition or an engine operation, with tests.
/design-abm, /set-brand-split, /tune-economicsDesign an ABM motion, set the brand and activation split, tune with real unit economics.
/go-live-checkGO 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).

IdFitsDistinctive pieces (definitions seeded)
b2b-saas-sales-ledB2B SaaS with a sales teamAccount identity rule, opportunity stage machine, compliance policy, two personas, lane sales-outbound (34)
enterprise-abmNamed accounts, buying committeesAccount-journey stage machine, three personas, committee model, lane abm-multithread, play account-play (34)
b2b-saas-plgProduct-led, product-qualified leadsSegment plg-active-workspaces, lane plg-nurture, metric pql-accounts (17)
devtoolsDeveloper tools, developer relationsPersonas developer and eng-leader, lane devrel (16)
agency-servicesAgency selling services to small businessesOutside-in scoring, lane services-outreach, bounce-rate monitor (21)
b2b-marketplaceTwo-sided marketplaceSupply and demand personas and segments, lane supply-acquisition (18)
dtc-b2cConsumer brand, repeat purchaseLane lifecycle-email, definition.unit_economics default, play winback, ad pacing (14)
outboundJust outbound email to startPersona, segment, scoring, lane primary-email, a signal_follow_up sequence and signal-to-sequence plays (19)
seoJust SEO and GEOPriority-pages segment, freshness and volume monitors, daily-seo-audit. Requires Google Search Console (15)
full-gtmEverything, including SEO and adsThe 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

  1. Claude lists and reads: blueprint.list, then blueprint.get with the id.
  2. Claude applies it with blueprint.apply: id, env (default dev), senderPhysicalAddress (stamped on every email lane that has none), parameters, gscProperty (required for a search blueprint on a live instance), and optionally adaptToOnboarding. It first runs a validator. It drafts a change set of only the resources that differ, then proposes config.apply. Applying the same input again while that proposal waits returns the same proposal.
  3. 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.

CRM > Blueprints shows the catalog and each blueprint's detail and apply page. config.list with environment: "dev" shows what was seeded.
If it fails
  • VALIDATION_FAILED with 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_status says what is missing.

Customize after applying

  1. Read what was seeded (config.list, config.get).
  2. Change only what your interview requires: ICP criteria, weights, thresholds, lane caps, brand voice, play steps. Keep keys stable (default scoring model, lane keys) so plays and engines still resolve them.
  3. Propose, approve, sign, promote (Chapter 5). Chapter 7 gives the prompt for each kind of logic.
  4. 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.list and blueprint.get, recommends one (here likely enterprise-abm), and calls blueprint.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.md and setup-status.md up 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_status and runtime.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.

Chapter 7

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 questionIt lives inSection
Who is a fit, and what is a good signal?definition.scoring_model, definition.segment, definition.personaICP and scoring
When is an account qualified, and what happens next?definition.committee_model, flag qualification.handoffQualification
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.lanePlays, sequences, cadences
Who must we never contact, and what does the law require?definition.suppression_rule, definition.compliance_policy, guardrails, consentSuppression and compliance
What may we say, and in what voice?definition.brand_voice, the proof library, the claims policyClaims, voice, proof
When are two records the same, and who wins?definition.identity_ruleIdentity and merge rules
How do we measure it?definition.metric, definition.money, definition.data_monitorMetrics and money
What stages does a record move through?definition.journey, definition.stage_machine, definition.stage_mapStage machines and journeys
Every prompt here stops at proposals. None of them sends mail, spends money or promotes to production. Where an operation could, the prompt says to stop first. A human approval always sits between a proposal and a real effect.

Untested. This prompt was not run against a live instance. Operation names were checked against the 0.2.5 catalog.

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 changeWhere
Fit rules: country, size, industry, anything on the account, as a CEL condition with a weightdefinition.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 fadecomposite.intentHalfLifeDays, and per family in composite.intentHalfLifeByFamily (buying, intent, engagement, product, lifecycle, negative)
How much first-party evidence outweighs third-partycomposite.partyTierMultipliers (first, second, third, unknown)
The mix of fit and intent, and the tier namescomposite.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 bouncesnegatives: replyLabels[] with a penalty, and hardBounce
Different weights for different segments, with bands that decay without a rep touchsegmented block
Who a "persona" or "segment" isdefinition.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.get for both keys, drafts the new specs, validates them, runs score.rescan_preview (no writes) to show who would move into or out of qualified, then proposes config.apply.
You do
A different admin approves and, later, signs and promotes (Chapter 5).
After promotion, 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_fit is 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_organization re-scores accounts whose scores are stale or were made under an older model. Run score.rescan_preview first.
  • Learning. score.learn proposes 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.feedback records 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 changeWhere
The score at which an account is qualified or needs reviewqualifiedThreshold and needsReviewThreshold in the scoring model (see above)
The roles a healthy deal must cover, and the champion roledefinition.committee_model key default: requiredRoles, championRole, recencyWindowDays, and your own roles[]
Your vocabulary of buying rolesbuying_roles.set (human-only: publishes and signs the committee model), or the committee model by change set
What happens when an account qualifiesFeature flag qualification.handoff, set with flags.set (also Settings > Workflow automation)
How the engines react to a strong new signalFeature 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.

KeyMeaning
enabledTurn the whole handoff off with false.
routefalse 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.
enrolfalse to skip enrolment, or { sequenceId, laneKey, maxContacts }. maxContacts runs 1 to 10 (default 1).
disqualifiersrelationshipStatuses (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 with flags.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_model key default with requiredRoles, championRole and recencyWindowDays: 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.set overwrites 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_FAILED naming 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 changeOperation
A territory: name, kind (geographic, segment, named_account, vertical), match rules, owner or rotation, priority, activeterritory.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 windowrouting.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 ownerrouting.reconcile
See which territory a record would get, and whyterritory.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 with territory.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.assign with a named owner skips round robin. An inbound play should pass keepExistingOwner so a rep's lead is never taken, and skipCustomers so 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 changeWhere in definition.play
When it startstrigger.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 tofilter (CEL), customerTypes
What it doessteps[]: 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 preconditiongate on the play, or on one action. It never replaces human approval.
Limitsguardrails: maxActionsPerRun, maxBudgetUsd, rateLimit (maxPerWindow in windowMinutes)
Reportingoutcomes[]

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 changeWhere in definition.sequence
The steps (1 to 20), each on email or LinkedInsteps[]: key, channel, wait (0 to 90 days or business days)
The wordsA 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 sendsendWindow: start, end and ISO weekdays, in the recipient's local time
A/B testsvariants[] (2 to 5, each with a weight)
Depth of personalizationpersonalisation: auto, 1:1, 1:few, 1:many
LinkedIn actionslinkedin.action: message, connect, connect_then_message (with an accept wait and a timeout rule), inmail
When a person leaves itexits: 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.play with a schedule trigger and a deal.stage_stuck audience, one action step, and maxActionsPerRun. Runs play.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.sequence with sendWindow, two variants of weight 50, and exits.
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.lane in dev. You provide the sender postal address if it asks.
You do
Approve, sign, promote, then ask Claude to run lane.activate and have a different human approve it.
Operate > Runs shows each play run step by step. CRM > Sequences shows rosters, messages and replies. 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 30d does 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 changeWhere
Who is never contacted: competitors, customers, opted out, a do-not-contact listdefinition.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 enrichmentprivacy.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_approvalpolicy.guardrails.set (human-only; replaces the whole spec). Read with policy.guardrails.get.
Pre-send checks per lanedefinition.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: map

Add 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_policies for dev, proposes definition.suppression_rule rules by change set, and reads consent.get and the lane's enforcedChecks to 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 changeWhere
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 makeforbiddenClaims[] (up to 200): phrase, reason, match as phrase or word
The proof a claim can rest on: case studies, metrics, testimonials, logosThe 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 countproof.propose_activation (human-only; a different person approves). Each proof has a permission and an expiry.
What happens to a claim with no approved proofpolicy.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_voice with vocabulary.avoid and forbiddenClaims. Runs proof.extract_from_document to 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_approver mode, 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).

LayerWhat it doesOperations
Observation to entityDecides 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_ruleField
Which keys match, and how stronglyrules[]: key and role (authoritative, corroborating, forbidden)
Who wins a field when sources disagreesurvivorship.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 keepssurvivorship.mergePerProperty and mergeDefault, which also allow keep_survivor and manual
Match tuningmatching: 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 with identity_dq.merge_rules.propose, runs identity_dq.scan_duplicates and identity_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_records is proposed and a different human approves. It is reversible with identity_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 changeWhere
A derived KPI over any objectdefinition.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 scheduledefinition.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 playsattribution: windowDays (1 to 365), model (equal_split, position_based, first_touch, last_touch, time_decay)
The exchange-rate rulefx.convention: market_daily or finance_dated (finance's own rate sheet, money.rate_sheet.set, human-only)
The reporting currencycrm.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.metric versions (the second with parentKey), then runs metric.evaluate and metric.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, then money.reconcile and money.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_contradictions finds 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 changeWhere
A lifecycle for companies, people or deals: ordered stages, each open, won or lostdefinition.journey: objectType, stages[] (key, label, category), isDefault, priority, segments routed to it, optional pipelineId and stageMachineKey
The legal moves between states, with guardsdefinition.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 meansdefinition.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 customerdefinition.customer_type: CEL criteria, priority (a company takes the first type it matches), up to 3 journeys
Your words for deal typesdeal_type.set (human-only), or definition.deal_taxonomy by change set
Deal pipelines and their stagespipeline.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 linked definition.stage_machine), then stage_map.unmapped and stage_map.suggest.
You do
A different human approves journey.apply. You decide each stage mapping, in Settings > Journeys or by telling Claude to call stage_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_record is 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.
Chapter 8

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.

EnginePackage (Settings > Packages)ConsoleMode keyJob in one line
Scorestdlib/engine.scoreEngines > Score (its Lead queue page), CRM > Hot listcrm_writebackTurns signals and firmographics into a verdict, a tier and the reasons.
Outreachstdlib/engine.outreachEngines > Outreach, CRM > Sequences, CRM > Conversationsoutreach (and meetings)Drafts, sequences and sends email and LinkedIn steps through approved lanes.
Performance Adsstdlib/engine.performance-adsEngines > Performance Adsperf_adsPaces spend and moves budget toward your CAC or ROAS target.
SEO & GEOstdlib/engine.searchEngines > SEO & GEOseo_geoAudits pages, tracks AI answers, plans and publishes content.
Data Quality (Merges)stdlib/engine.identity-dqEngines > Merges, Operate > Monitorsidentity_dqFinds duplicates, validates and normalizes, merges with undo.
Enrichmentstdlib/engine.enrichmentEngines > EnrichmentenrichmentFills empty fields through a provider waterfall under a budget.
Signalsstdlib/engine.signalsCRM > Signals, CRM > Hot listnoneRecords 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.

Roadmap, not shipped. The "30 engines" list in the vision material is a plan. In 0.2.5 only the seven engines above are packaged. If a client asks for lifecycle, partner, ABM or customer-success engines, say plainly that they are on the roadmap and that today you build them from plays, definitions and agents on top of these seven.

The chain, from raw record to a reply

Goal: predict what a change in one engine does downstream.

  1. 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.
  2. Data Quality. identity_dq.normalize_records cleans domains, emails and casing. The duplicate scan proposes merges that follow your identity rule.
  3. Enrichment. Fills empty fields (employee count, industry, country, email, phone). Fit scoring reads employeeCount, countryCode and industry, so enrichment quality sets the ceiling for fit.
  4. 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 emits gtm.signal.observed.
  5. Score. Composite of fit (40 percent) and intent (60 percent) by default. A new qualification emits gtm.scores.computed.
  6. 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.
  7. 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.
  8. 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.

Pick one account and walk it: 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

WorkStartsWhere to change it
Re-score every stale accountDaily (score.rescan_organization), plus on a new signal, a verification change or a model changeFixed schedule. Preview with score.rescan_preview.
Score learningWeeklyscore.learning_settings.set (human only)
Signal scan (looot research)Daily; on demand with signals.scan_nowFeature flag signals.scan
Data quality scanDaily (identity_dq.scan), on demand tooFeature flag identity_dq.thresholds, definition.data_monitor
Cadence rescan (next sequence step)Hourly, and when a sequence is activateddefinition.sequence
LinkedIn acceptance and reply pollingHourlyFixed
Ads allocatorNightly at 02:00 UTC; perf_ads.reallocate on demand (once per hour)definition.unit_economics
Ads campaign scan, SEO page auditScheduled 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 visibilityOff until enabled; then every 24 hours by defaultseo_geo.update_visibility_settings
CRM syncEvery 6 hours once a CRM of record is setCRM sync settings
EnrichmentOn 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.

EventWhenFields to filter on
gtm.signal.observedA signal is recordedsignalId, kind, category, accountId, contactId, source
gtm.signal.verification_changedA person verifies or disputes, or a signal is retracted or restoredsignalId, kind, toStatus, released, accountId, contactId
gtm.scores.computedAn account gets a new qualification (not for an unchanged re-stamp or a dry run)accountId, qualificationId, verdict, fitAvailable
gtm.reply.receivedA reply is recorded on any channelreplyId, channel, sourceSystem, contactId, accountId, dealId, campaignId, sourceCategory
gtm.meeting.bookedA meeting appears on a connected calendarmeetingId, title, startsAt, accountId, source, sourceCategory
gtm.record.createdA record is createdobjectType, recordId, source, sourceCategory
gtm.deal.stage_changedA deal changes stagerecordId, fromState, toState
gtm.journey.stage_enteredA record enters a journey stageobjectType, recordId, journeyKey, stageKey, category
gtm.consent.changedA contact's consent changescontactId, channel, purpose, state
gtm.dry_draft.producedDry Mode held a new draftledgerEntryId, engine, actionType, reason
gtm.sync.completedA sync finishesmonitorRunId, 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).

Rules every engine shares

Goal: know the guardrails before you tune an engine, so a tuned engine cannot hurt a client.

Dry and live, per engine

  • A new organization starts with every engine dry. A dry engine watches, scores and drafts, but sends and writes nothing outside. Each draft is a drafted row in Operate > Actions with its reason.
  • Organizations that existed before 0.2.5 keep their old behavior: an engine with no row of its own follows the organization-wide Dry Mode.
  • The organization-wide Dry Mode is the master. An engine runs in the stricter of the two.
  • Needs a human An owner or admin takes an engine live in Settings > Safety, or with policy.engine_modes.set, which agent keys cannot call. Read the state with policy.engine_modes.get.

Guardrails across all engines

Set with policy.guardrails.set (human only; pass the complete spec and the expectedVersion from policy.guardrails.get). A reached cap or quiet hours hold the action. It is never dropped, and it runs by itself later.

KeyWhat it limitsDefault
emailsPerDayOutbound emails per organization day, across enginesno limit
newAccountsPerWeekAccounts that get their first outbound message per ISO weekno limit
adSpendPerDay {amount, currency}Total daily ad budget across active campaigns. Above it a person must approve.no limit
sendWindow {start, end, days}When messages may go out, in the recipient's local timenone
allowedChannelsOnly these channels: email, linkedin, ads, sms, whatsappno restriction
customCaps[] {key, label, match, channel, window, limit, onHit}Your own cap on any action type. onHit is defer or require_approval.none
timezone, fallbackTimezoneWhere the counters roll over, and the zone used when a recipient has noneUTC

Rules no setting turns off

  • Net-new rule. A first message, or a meeting invite, to a contact nobody has ever written to is never auto-sent. It always waits for a human, at every autonomy level.
  • Claims gate. A stat, a result or a named customer in outbound copy must be backed by active proof in Settings > Proof library. It runs when the action is proposed and again right before it runs. It covers outreach, SEO page publishes and meeting invites.
  • Four-eyes. The person who proposed an action cannot approve it. An agent never approves.
  • Only signed definitions run in prod. An engine reads the signed version in the call's environment, and falls back to its built-in default when there is none.

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 knobHow it changesWhoExamples
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 humanscoring_model, lane, sequence, brand_voice, unit_economics, identity_rule, binding
Definition, sign-exemptActive on publishBuilderdata_monitor
One-step policy opThe op publishes and signs as the calling owner or admin. Agents can read it but not set it.Owner or admin. Needs a humanenrichment.policy.set, signals.settings.set, score.learning_settings.set, policy.engine_modes.set, policy.guardrails.set, routing.member_settings.set
Feature flagflags.set {key, value}, or Settings > Feature flags and Settings > Workflow automationOwner or adminsignals.scan, signals.reaction, qualification.handoff, identity_dq.thresholds
Provider setting (not secret)connections.set_provider_setting {provider, key, value}Owner or admingsc/site_url, google_ads/customer_id, cms/target
Per-call inputAn argument on one run of an opWhoever runs itmaxUsd, estimate, maxEntities
Fixed in codeCannot be changed from the console or by Claude. Say so honestly.Needs a releaseAds 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.list and runtime.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.
Chapter 9

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_fit model.
  • 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

ScreenUse it for
Engines > ScoreTabs 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 listAccounts 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 automationSwitch the qualified-account handoff on or off.
Settings > SafetyDry 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.

KnobKeyDefaultNotes
Intent rulesdefault: rules[].matchesKind, weight, optional condition, optional partyTiers13 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 -15A signal counts only if a rule names its kind. A kind with no rule adds nothing.
Verdict thresholdsqualifiedThreshold, needsReviewThreshold60 and 30 (fit model: 70 and 30)Apply to the composite when verdictBasis is composite.
Fit and intent mixcomposite.fitModelKey, fitWeight, intentWeight, intentCeilingicp_fit, 0.4, 0.6, 100verdictBasis: composite or intent.
Decay per signal family (days)composite.intentHalfLifeDays, intentHalfLifeByFamily30; buying 45, intent 14, engagement 21, product 21, lifecycle 60, negative 60A signal loses half its weight every half-life.
Who observed itcomposite.partyTierMultipliersfirst 1.5, second 1, third 1, unknown 1 (each 0 to 5)First party is your own site, forms, mailbox, CRM.
Priority tierscomposite.tiers[] {id, label, minScore}A Hot 75, B Warm 60, C Nurture 30, D Cold 0Console reads these labels.
Hard disqualifiersdisqualifiers[] {id, statement, cap, when}One: score.default.dq.competitor, cap 0when takes relationshipStatuses, field {path, in or notIn}, signalKinds with withinDays, or suppressionReasons. The competitor rule does nothing until competitors are listed in setup.
Negative behaviornegativeswindowDays 180, halfLifeDays 60; unsubscribe 30, objection 10, wrong_person 5; hardBounce.penalty 15Points on the 0 to 100 intent scale. Labels come from your definition.reply_labels.
ICP fit rulesicp_fit: rules with matchesKind: [firmographic] and a CEL conditionNeutral, size only: 10 or more employees 40, 50 or more 40, 50 to 5,000 another 20Fit rules can read only employeeCount, countryCode, industry and marcomTeamSize. With no employee count, fit is unavailable and the composite rests on intent.
Buying committeedefinition.committee_model key default: requiredRoles, championRole, recencyWindowDays, roles[]none until you or a blueprint sets oneRead by score.committee_coverage and the segmented committee_coverage signal.
Segments and bands (advanced)segmented: segments, signals, bands, bandDecay, repTouch, contactGrainNot 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).
HandoffFlag qualification.handoffenabled 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 enrolmentroute: false or enrol: false turns a step off.
Learningscore.learning_settings.set: autoPropose, outcome, modelKey, horizonDays, maturityDaysreport only; outcome opportunity; model default; 60 and 60Learning 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: string

Operations

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.computed for 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.observed and gtm.signal.verification_changed through the signal reaction (see Signals).
  • Ledger rows for write-back: crm.outcome_written and crm.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 whose matchesKind names 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_field feature.
  • Account-based tiers. Add a committee_model and the segmented committee_coverage signal for enterprise accounts.
  • Compile a client rule. The /compile-rule slash 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_fit model with config.get, proposes a new version with config.propose_changeset, runs config.validate_changeset and config.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) and partyTierMultipliers.first (2) in the default model. Calls score.rescan_preview after it is signed in dev.
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, and when.signalKinds: [risk] with withinDays: 30 and 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].penalty and negatives.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}, then score.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

Run 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 bounce reply label and hardBounce. 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

ScreenUse it for
Engines > OutreachTabs KPIs, Pending sends, Contacts, Draft, Propose send, Advance cadence. Funnel tiles, a Deliverability panel, a sending lanes panel, What's working.
CRM > SequencesSequence list. A sequence has tabs Steps (plan editor with preview), Members, Messages, Replies.
CRM > ConversationsEvery reply with its label and its proposed next action.
Operate > Needs a human, ApprovalsApprove sends and next actions. The approval shows a Claims panel.
Settings > Brand & compliance, Proof library, Connected accounts, SafetyBrand safety rules, claims check mode, mailboxes, dry or live.

What you can customize

KnobKeyDefaultNotes
Lane: volume and cadence of touchesdefinition.lane: dailyCap, recentTouchWindowHours, openDealPolicy (block or allow), envelopeBudgetEur, windowDaysnone set. The services blueprint uses 200, 336, block, 200, 30.A send needs an activated lane. An un-laned send is refused.
Lane: checksenforcedChecks: suppression, consent, mailbox_health, daily_cap, template_integrity, declared_params, recent_touch, open_deal, reply_stopall checks run when the field is omittedListing checks narrows the set. Only list what you intend to keep.
Lane: auto-pausebounceRatePauseThreshold, complaintRatePauseThreshold (0 to 1)none. Blueprint: 0.04 and 0.001Rates show only after 10 sends.
Lane: sender identitysendingDomain, dkimSelector, senderPhysicalAddress, temperature (cold or warm)nonelane.activate checks SPF, DKIM and DMARC. In a prod-class environment it refuses unless all pass.
Lane: LinkedIn seatlinkedin.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 stepsdefinition.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), personalisationWithout a step plan: one step every 3 daysWaits count in the recipient's timezone. Only a signed plan runs in prod.
Sequence exitsexits: reply, unsubscribe, bounce, manualStop (always on), meetingBooked, stageChange {objects, stages}meetingBooked trueChecked at propose and again at execute.
LinkedIn step actionsteps[].linkedin.action: message, connect, connect_then_message, inmail; note, acceptWaitmessage; accept wait 14 days, then next_step or exitLinkedIn sending is off until an owner accepts the risk (broker.accept_high_risk).
Personalization depthpersonalisation: auto, 1:1, 1:few, 1:manyauto: ABM tier (abmTier or tier on the account), else score band (hot 1:1, warm 1:few, cool or cold 1:many), else 1:11:many renders a template with merge fields and uses no model call.
Brand voicedefinition.brand_voice key default: tone, vocabulary.preferred, avoid, examplePhrases, voiceSamples (up to 20), forbiddenClaims (up to 200)noneAlso used by SEO drafts.
Reply labelsdefinition.reply_labels: labels[] with phrases, patterns, senders, sentiment, priority, effects {sequence, consent, signalKind, openAsk, followUpDays}; modelAssist, minConfidencemodelAssist true, minConfidence 0.6A person's label always wins.
Who is never contacteddefinition.suppression_rule (CEL criteria, reason), definition.compliance_policy (rules[])blueprint rulesChecked on every send.
Signal playsoutreach.signal_enroll: dailyCap, recentReplyDays, kinds, stepsKey, laneKey25 enrolments per organization per UTC day; skip anyone who replied in the last 90 daysSkips customers, partners and open opportunities. Needs an active sequence bound to a lane and a signed plan.
Claims checkSettings > Proof library > Claims checkblock (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 guardrailspolicy.guardrails.set: emailsPerDay, newAccountsPerWeek, sendWindow, allowedChannels, customCapsno limitsSee 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 proposes outreach.reply_next_action (book a meeting, snooze, referral). Unsubscribe and bounce propose nothing, because suppression already ran.

Dry Mode and guardrails

  • Engine keys outreach and meetings start dry on a new organization. Dry drafts appear in Operate > Actions and emit gtm.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.sequence per 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-play for a new signal kind.
  • New reply behavior. Add a label with its effects in definition.reply_labels.
  • Other rails. outreach.sending_mailboxes lists 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.sequence and calls sequence.create with stepsKey (the sequence waits until the plan is signed). Does not call sequence.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.lane with dailyCap: 50, openDealPolicy: block, bounceRatePauseThreshold: 0.03, complaintRatePauseThreshold: 0.001, temperature: cold, senderPhysicalAddress. After signing, calls lane.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 for policy.guardrails.set. Agent keys cannot set it, so Claude gives you the exact spec. Reads policy.guardrails.usage after.
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_voice key default: 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_labels with sentiment: neutral, effects.sequence: stop and effects.openAsk {title, dueDays: 60}.
You do
Approve and sign. Send a test reply through replies.ingest in dev and check replies.get shows 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 enforcedChecks by 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.reaction to enrol: false. Otherwise the second path skips with in_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

ScreenUse it for
Engines > Performance AdsTabs 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 > MoneyRevenue truth and the attribution window and model.
Settings > SafetyDry or live for perf_ads, and the daily ad spend guardrail.
Integrations > ConnectionsGoogle Ads and Meta Ads (native or through Composio).

What you can customize

KnobKeyDefaultNotes
The targetdefinition.unit_economics key default: targetCac, or targetRoas, or maxPaybackMonths (max 120) with grossMargin and monthlyRevenuePerCustomernoneThe older cac field is not a target.
Other economicsltv, newCustomerAov, costOfDelivery, repeatPurchaseRate, amerCurve[], currencynoneFeed the first-order profit gate and the spend ceiling.
Attributiondefinition.money: attribution.model (equal_split, position_based, first_touch, last_touch, time_decay), windowDays (1 to 365), positionWeights, halfLifeDaysequal splitApplied when a report is read. Switching recomputes every report.
Revenue truthdefinition.money.revenueTruth: crm_deals, or a warehouse or billing objectCRM deal amountsCAC and ROAS are only as good as this.
Allocator tolerance bandperf_ads.reallocate {band}0.2 (0.05 to 0.5)A campaign is "worse" or "better" only outside the band.
Allocator stepmaxStepPct0.2, and 0.2 is the hard maximumPer campaign per run, and per UTC day from the start-of-day budget.
Evidence before judgingminSpend, minConversions, minCustomers, minCampaignAgeDays, windowDays250, 5 (or 2 customers), 2 customers to receive budget, the attribution window, the larger of 30 days and the attribution windowPer-run inputs on a manual run. The nightly run uses the defaults.
Cool-downcoolDownDays7 (0 to 60)A campaign changed within this time is not moved.
Daily budget ceilingpolicy.guardrails.set: adSpendPerDay {amount, currency}no limitThe allocator moves nothing while projected daily budget is above it. Human only.
Channels allowedguardrails allowedChannels (include ads)no restriction
Ad accountsconnections.set_provider_setting: google_ads/customer_id, google_ads/login_customer_idnoneMeta needs no account setting. In live mode the connector reads only this.
Manual change capperf_ads.update_campaignAn increase is capped at 2 times the current budgetA larger value is rejected.
Pacing thresholdsFixed in code: overspend pace 1.3, underspend pace 0.3fixedNot 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: 2400

Set 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, with perf_ads.undo_campaign_change as 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_campaigns from 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_economics key default with targetCac: 250, grossMargin: 0.7, currency: EUR. After signing, calls perf_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.get and prepares adSpendPerDay: {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_runs and perf_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_campaign that 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_performance over 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

ScreenUse it for
Engines > SEO & GEOTabs 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 libraryThe proof the claims gate and the content briefs cite.
Integrations > Connections, Settings > Connected accountsGoogle Search Console, GA4, Ahrefs (through Composio), your CMS.

What you can customize

KnobKeyDefaultNotes
Search Console propertyconnections.set_provider_setting {provider: "gsc", key: "site_url"} (or gtm apply --gsc-property)nonesc-domain:example.com or https://www.example.com/. Required. seo_geo.gsc_properties lists what the connection can read.
Tracked prompts and keywordsseo_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, activeall engines, US, en, activeEngines: 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 visibilityseo_geo.update_visibility_settings: enabled, cadenceHours (1 to 720), rankDropThreshold, visibilityDropThreshold (0 to 1), maxRunCostUsdoff; 24; 3 positions; 0.15; 1 USDPaid. Enabling on a live instance needs Search Console. A run stops when it reaches the cap.
Cluster buildseo_geo.build_clusters: method (auto, serp_overlap, embedding, lexical), embeddings (skip or use), maxCostUsd, minImpressions, maxItems, daysauto, skip, 0.25 USD, 5, 500, 28Free unless you opt into embeddings. seo_geo.cluster_sources shows the estimate first.
Brand voice and forbidden claimsdefinition.brand_voice key defaultnoneDrafts are written in it, and QA fails on a forbidden claim or avoided word.
Claims check modeSettings > Proof library > Claims checkblockIn route-to-approver mode QA only warns and the approver acknowledges at publish.
CMS targetProvider cms: target, site_url, webflow_collection_id, webflow_field_map, wordpress_com_site_id, webhook_endpoint_idnoneTargets: 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 rulesProvider cms: refresh_traffic_drop_pct, refresh_age_days20 and 180Also fixed: ranking down 3 places, or a tracked AI prompt no longer cites the page.
Keyword dataseo_geo.refresh_keyword_dataoff until runNeeds looot. Without it the Keywords tab still shows Search Console and Ahrefs data.
QA gateFixed in codereading ease 30 or more, 30 words or fewer per sentence, 150 words minimum, a title, no placeholder leftNot 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) and seo_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_pages and seo_geo.mark_refresh_due from 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_prompt three times with kind: ai_prompt, brand, brandAliases, competitors, targetUrls: ["example.com"] and engines: [chatgpt, perplexity, google_ai_overview]. Lists them back with seo_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, then connections.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, then seo_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 maxRunCostUsd and keep engines narrow.
  • 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

ScreenUse it for
Engines > MergesMode, 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 > MonitorsData monitors, their runs, lane resume.
Operate > Needs a humanUncertain identity matches wait here.
Settings > SafetyDry or live for identity_dq (Duplicate merges).

What you can customize

KnobKeyDefaultNotes
Identity keys and their roledefinition.identity_rule per objectType (gtm.account, gtm.contact): rules[] {key, role}; role is authoritative, corroborating or forbiddenBuilt-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 weightsmatching.weightssame_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.5Only the reasons you name change.
Minimum confidencematching.minConfidence (0 to 0.99)0 (every matched pair is proposed)Raise it to cut noise.
Name similarity floormatching.nameSimilarity (0.5 to 1)0.82Used for the similar_name reason.
Which value wins on a mergesurvivorship.mergeDefault and survivorship.mergePerProperty. Strategies: keep_survivor, most_recent, highest_confidence, first_write_wins, longest_non_empty, source_priority (with order), confidence_weighted, manualKeep the survivor's values and fill only its empty fieldsmanual blocks approval until a person picks the value. Merge-only rules never change how incoming data is projected.
Which value wins on incoming datasurvivorship.default, survivorship.perPropertymost_recent in the blueprintsSince 0.2.5 a perProperty rule also decides a merge unless a merge-only rule exists for the field.
Quality alert thresholdsFeature flag identity_dq.thresholds: maxDupRate, minMeanQuality, maxStaleRate0.05, 0.5, 0.8Set with flags.set. A breach marks the daily run breach and notifies the owner.
Staleness window and scan sizestaleDays and limit inputs on identity_dq.quality_report; limit on the scans30 days; 1,000 records (max 5,000)Per-call inputs.
Data monitorsdefinition.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, autoPauseLaneblueprint monitorsSign-exempt: active as soon as it is published, even in prod.
Normalization rulesFixed in code: domain, email, casing, whitespacefixedNever 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.9

Operations

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_alert notification, 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_monitor instances per lane (bounce rate, complaint rate) or per property (fill rate on industry).
  • Run it after every import. A play on gtm.sync.completed can 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, then identity_dq.merge_rules.propose with matching {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, then identity_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} with flags.set and publishes a definition.data_monitor of type fill_rate with property: 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 manual needs 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

ScreenUse it for
Engines > EnrichmentMode, fill rate, spend, bindings, Freshness windows, Monthly cap, newer enriched values and runs.
CRM > Companies, ContactsSelect rows, choose Enrich, read the cost preview, confirm. A record's Enrichment panel lists Newer enriched values with Propose update.
Integrations > ConnectionsThe looot connection (an org key, or the instance key as a fallback).
Settings > SafetyDry or live for enrichment (Enrichment spend).

What you can customize

KnobKeyDefaultNotes
Freshness window per field (days)definition.enrichment_policy: ttlDays (map field to days, 1 to 3650), defaultTtlDaysemail primaryEmail 90, custom.emailStatus 90, phone 180, domain, industry, employeeCount, countryCode 180, custom.technographics 90, custom.personality 365; other fields 180Set with enrichment.policy.set: it publishes and signs in one step (human only). enrichment.policy.get lists the fields.
Monthly spend capenrichment.policy.set {monthlyCapUsd} (stored in the policy)no capCounts every enrichment lookup this UTC month. 0 means only free cached answers. null removes the cap.
Provider per capabilitydefinition.binding key enrich.<capability>: backend, opId, fallbacks[], maxUnitCostUsd, verificationRequired, regionConstraintslooot job bindings from the blueprints, for example enrich.find_email to job:people.email.find with maxUnitCostUsd 0.05Capabilities: 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 orderCatalog order per field, reordered by hit rate and cost learned from your ledgerFor employeeCount: enrich.headcount then enrich.companyEqual evidence keeps the catalog order. Not directly settable.
Per-call price ceilingInstance setting LOOOT_MAX_COST_USD, and the binding's maxUnitCostUsd0.05Above it the call waits for an approval.
Per-run size and spendenrichment.enrich_accounts and enrich_contacts: maxEntities, ids, estimate, maxUsd, requireMonthlyCap25 records, max 500maxUsd: 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 runenrichment.refresh_stale: objectType, maxEntities, ids, fields, estimate, maxUsd25 recordsTouches only values enrichment wrote and the record still holds. Never a person's value.
Overwrite a person's valueenrichment.overwrite_field: confirmHumanValueoffNeeds 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) and enrichment.field_overwrite_undone.
  • Every lookup is a line on the cost ledger (Operate > Cost). Each value carries custom.enrichment[field] with verifiedAt, ttlDays, confidence, provider.
  • enrichment.policy.set emits gtm.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 marked dryMode: true and attempts no lookup. If Dry Mode turns on during a run, held lookups are counted in heldByDryMode, 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_stale with maxUsd and requireMonthlyCap: true.
  • Enrich on create. A play on gtm.record.created that calls enrich_accounts with requireMonthlyCap: 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} and enrichment.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}, then enrichment.coverage_report and enrichment.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.binding key enrich.find_email with backend: looot, opId, maxUnitCostUsd: 0.04, a fallbacks[] entry on backend: mcp with maxUnitCostUsd: 0.02, and verificationRequired: 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.

The email fallback prompt assumes an MCP server named emails is already registered (chapter 10).

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: true first.
  • 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 maxUsd or 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

ScreenUse it for
CRM > SignalsThe 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 listSignals behind each score. Scan for signals now runs the scan on demand.
Settings > Feature flags, Workflow automationThe scan, the signal reaction and the qualified handoff.

What you can customize

KnobKeyDefaultNotes
Verification gatesignals.settings.set {mode}: off, seller_surfaces, strictNew workspace: strict. Workspace from before 0.2.5: offStrict: only verified signals count and reach sellers. Moving to or from strict re-scores every account. Owner or admin, human only.
Auto-verify rulesdefinition.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 kindSame 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 -25The first rule that names a kind wins. A kind with no rule scores defaultWeight.
Strength modelsignalStrength: defaultWeight, halfLifeDays, disputedMultiplier, floor, ceil5, 30, 0.5, 0, 100A disputed signal keeps half its weight. A retracted one is dropped. Bands: 60 and up hot, 30 and up warm.
Signal class per kindsignalStrength.classes (buying, growth, risk, other)derived from the kindUsed by the feed filter.
Scan: on and sizeFeature flag signals.scan: enabled, maxAccounts (1 to 500), perRunCapUsd (0 to 1000), capabilities, relationshipStatuses, lookbackDays, headcountChangeThresholdtrue; 10; 1; all eight; ["prospect"]; 90; 0.1Capabilities: 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 detectionsignals.ingest_intent {thresholds}: zThreshold, floorz 2, floor 0, at least 4 samplesCapabilities: intent.topic_surge, intent.visitor_identification, intent.review_site_intent.
What a signal does on arrivalFeature 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 engagementStrength 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: -40

Check the provider string stored on a real looot signal before you rely on the auto-verify rule above.

Operations

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.observed for every new signal and gtm.signal.verification_changed on 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.enabled is false. The page says which one applies.
  • The scan stops at perRunCapUsd through 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.record with partyTier: "first". Third-party sources: set partyTier to third.
  • 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 defaultWeight in 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 signals model with config.get, adds signalStrength.autoVerify rules, 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_pricing rule weight, funding weight, and signalStrength.halfLifeDays: 14 in the full copied model.
You do
Approve and sign. Check with signals.score on 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.set for signals.scan with maxAccounts: 50, perRunCapUsd: 5, lookbackDays: 60 and the six remaining capabilities. Then signals.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.record with partyTier: "first" and evidence, then signals.score. If no rule names demo_requested, it says it scores defaultWeight here and counts for nothing in Score.
You do
Decide whether to add a rule in both models.

demo_requested is not a built-in signal kind. The closest built-in kinds are form_submitted and discovery_call_booked.

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.enabled is 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 enrol off in signals.reaction.
  • "A signal I recorded does nothing." Score counts only kinds that its intent model names. Add the rule there too.
Chapter 10

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.

DirectionDoorWhat it is
InCSV importCRM > Import. Operation canon.import. Companies, contacts, deals.
InCRM mirrorYour CRM of record, read through Composio into companies, contacts and deals. crm.sync.
InAny other appA 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).
InInbound webhooksSigned POSTs from forms, Cal.com, Resend and Unipile.
InBought datalooot, only through capabilities and bindings (capability.quote, capability.acquire).
OutCRM write-backcrm.update_record and source.update_record. Proposed, a different person approves.
OutOutbound webhookswebhooks.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.

PathUse it forDo not use it forWhere you set it up
NativeModel 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.
ComposioYour 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.
loootThe 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 serverAn 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.readiness and connections.list (both reads).
You should see
Statuses live, configured_unverified, failed, fixture (demo mode) or not_configured, grouped as models, data, seo, outreach, linkedin, crm and ads. Each item says whether it is required or informational.
You do
Nothing yet. From a terminal, gtm doctor --strict gives 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.

  1. 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 key ak_.... A uak_ user key is refused.
  2. Ask Claude to link your apps (prompt below). In Connect mode, apps you already connected in your Composio account become ACTIVE at once.
  3. Needs a human A new app needs the hosted sign-in. Claude gives you the link. It lasts about 10 minutes.
  4. 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, then crm.sync_status.
You do
Connect Composio and the CRM app if they are not ACTIVE yet. crm.settings.set needs 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_CONFIGURED on crm.settings.set or crm.sync: no CRM of record, no Composio credential, or the CRM is not ACTIVE. 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_, then ak_, then the instance COMPOSIO_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
  1. Needs a human Add the looot connection in Integrations > Connections (Add a connection), then run Test.
  2. Ask Claude to find candidate provider endpoints and price them before it pins one.
  3. 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) and enrichment.budget.get. From a terminal it can also use gtm capabilities discover enrich.find_email --backend looot and gtm 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's maxUnitCostUsd). 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.

  1. 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.
  2. Needs a human You mark each tool Read or Write (approval). An unmarked tool cannot be used. Agents cannot do this step.
  3. 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 proposes definition.binding with backend: "mcp" and opId: "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 effectHow it runs
ReadThrough capability.acquire.
WriteNeeds 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

  1. CRM > Import. What are you importing? Companies, contacts or deals.
  2. Choose the file or paste CSV. The first row is read as headers. Comma, semicolon and tab all work.
  3. 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.
  4. Preview. Every row reads Create, Update, Held for review, or an error. Nothing is written.
  5. Commit. Owner or admin by default.

The rules that decide what happens

RuleWhat it means for you
Match fieldAn 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 cellsA blank cell never overwrites a stored value.
Company columnOn 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 companyWith 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 namesA 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-alikesA 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 limit1,000 rows per call. Each row commits in its own transaction, so one bad row never stops the rest.
Who may commitOwner or admin. Members only if crm.settings.set {memberBulkWrites: true}. Anyone can preview.
Agent commitsAn agent may preview. An agent commit becomes a proposal a different person approves.
Field namescamelCase 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.import with mode: "preview" and an explicit mapping. 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.
After a commit: "Import complete" with a per-row result, and in the result 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 on memberBulkWrites.
  • 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:

FieldWhat it sets
sourcebackend (composio or mcp), toolkit, listTool, args, pagination (none, cursor, offset or page), itemsPath, maxRecords, optional count and incremental (sinceArg, cursorField, format, lookbackMinutes).
target.objectTypeFor example gtm.account, gtm.contact, gtm.deal, gtm.signal, or a custom object.
idField, matchThe 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).
modemirror or append. Signals must be append.
schedule{kind: manual} or {kind: interval, everyHours, incrementalEveryMinutes?}.
writeBackOptional. 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 through config.propose_changeset, config.validate_changeset and config.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_status counts: 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.

StepOperationWho
1. Introspect a read list tool, liveadapter.startOwner or admin, or their agent
2. Completeness audit (proposing is refused until it ran)adapter.auditOwner or admin, or their agent
3. Propose lines with confidence and reasoningadapter.proposeOwner or admin, or their agent
4. Decide each numbered questionadapter.resolve, adapter.updateHumans only
5. Publish through the signed definition flowadapter.publishAgent may propose it. A different person approves, and a person signs.
6. Activate: nightly full read, incremental cadence, first full syncadapter.activateHumans only, after signing
Read the resultadapter.get, adapter.list, adapter.docAny 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 with adapter.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_CONFIGURED on adapter.start: the app has no ACTIVE Composio 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.
  • CONFLICT on adapter.update: someone edited it first. Re-read with adapter.get and send the new expectedVersion.
  • 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 is crm.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_map for 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 modes in_zone, soft_merge and federate never 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.

OperationForInputUndo
crm.update_record (E)The CRM of recordrecordType (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 appmappingKey, 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.

SourceEndpointHeader and secretResult
Website formsPOST <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.comPOST <api>/webhooks/cal-com/<orgId>x-cal-signature-256. Secret from the org's cal_com connection.discovery_call_booked signals.
Resend delivery eventsPOST <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.
ComposioPOST <api>/webhooks/composiowebhook-signature. Secret is the instance COMPOSIO_WEBHOOK_SECRET.Verified Composio events.

Outbound

  1. 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.
  2. 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.
  3. Your endpoint verifies the HMAC-SHA256 signature from the webhook-id, webhook-timestamp and webhook-signature headers 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), then webhooks.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.list after 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.

Only the event names in the reference are confirmed. The full set of event types that webhooks.create accepts is not published, so test a new subscription before you rely on it.

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_FAILED on webhooks.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.

FeedConnect throughOperation that pulls it
Firmographics, emails, phoneslooot binding (or your own MCP server)enrichment.enrich_accounts, enrichment.enrich_contacts, enrichment.refresh_stale
Call transcripts (Fireflies, Gong, Zoom, Fathom)Composiotranscripts.sync. Repeats every 6 hours. Synced transcripts are private by default.
Product events (PostHog, Mixpanel, Amplitude)Composioproduct_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, lemlistComposiooutbound_rails.sync_replies. Repeats hourly.
Website visitsA tracking snippettracking.site.create, tracking.verify, tracking.activity, tracking.sources
Coverage of all of thesen/asources.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.

  1. gtm doctor --strict exits 0, or instance.readiness shows the seam as live.
  2. crm.sync_status or source.sync_status shows counts, truncated: false and no errors.
  3. CRM > Data health has no object flagged below the threshold.
  4. 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/.env after 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.
Chapter 11

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.

LayerWhat it isWho runs itWhere it lives
Your coding agentClaude 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 agentsAgents that run inside GTM OS on a trigger (schedule, event, manual). Each has scopes, skills and hard bounds.The GTM OS workerPacks. Settings > Agents
SkillsTwo 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/aBoth listed in Skills
WorkflowsPlays: a trigger, a filter and steps that are operations. See Workflows.The flow runtimedefinition.play
MissionsA durable, human-approved plan an agent executes inside a budget.You or Claude advance it, one step at a timeMCP 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.

PackAgent keyAutonomymaxStepsmaxCostUsdDeadlineStarts
stdlib/pack.crm-agentsinbound-responderpropose151.005 minA form_submitted signal
meeting-prep-agentpropose121.003 minA meeting is booked
follow-up-agentpropose152.005 minA transcript is ingested
pipeline-review-agentpropose202.005 minMondays 07:00 UTC
deal-desk-agentpropose100.503 minA deal changes stage
stdlib/pack.revenue-agentssignal-watch-agentpropose303.005 minWeekdays 07:00 UTC
routing-steward-agentpropose200.505 minHourly 09:00 to 17:00 UTC, weekdays
scoring-steward-agentpropose202.005 minMondays 05:00 UTC
reply-triage-agentpropose201.005 minA reply is received
stdlib/pack.data-agentsenrichment-agentpropose251.005 minMondays 06:00 UTC
data-hygiene-agentpropose301.005 minNightly 02:00 UTC, and when an account is created
stdlib/pack.growth-agentsseo-geo-watch-agentpropose252.005 minDaily 06:00 UTC
stdlib/pack.ops-agentsonboarding-agentread301.005 minManual
ops-copilotread (read only)151.003 minManual
stdlib/pack.core-agentssdr-agentpropose255.005 minA verified funding, hiring or intent-surge signal
research-agentread153.003 minManual

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), then packages.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 example stdlib/pack.ops-agents/agents/ops-copilot, with status active. Triggers appear in agent.trigger.list only 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
LimitRule
maxSteps, maxCostUsd, deadlineMsIndependent 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 overrideMay lower a bound, never raise it. The lowest value wins.
authorityScopesMust 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.
TriggersChoose when a run starts. They can never raise bounds, change scopes, or change who the run is for.
AutonomyThe 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).
EffectsA 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 bumpagent.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.
RuleDetail
AllowlistThe 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 packageA 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 spendNo 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 publishedEach 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.

  1. Claude scaffolds a declarative package (gtm pkg scaffold acme/sales-pack --tier declarative) with contributes entries for skills, agents and triggers, and permissions.operations as the union of the skills' operations.
  2. Claude validates and tests it: gtm pkg validate, gtm pkg test (checks the declared eval files exist), gtm pkg build.
  3. Needs a human You install and enable it: gtm pkg install, then gtm pkg enable acme/sales-pack --env <default env>. Each is a proposal a different person approves.

Path B: register directly

  1. Needs a human An owner, admin or builder registers the agent: agent.register {name, specRef, scopes}. The caller becomes its owner.
  2. Claude proposes the definition.skill and definition.agent resources (key = the agent id). They go through config.propose_changeset, config.validate_changeset, config.apply.
  3. Needs a human A different person signs both (definition.sign). In prod, only a signed version is read. Promote with env.promote.
  4. Claude registers a trigger (next section).

Untested: the specRef value for an agent that is not shipped by a package was not confirmed. Prefer Path A.

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.agents lists 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.

TypeFieldsNotes
manualnoneFire it with agent.trigger.fire.
schedulescheduleCronFive-field cron in UTC. Lists, ranges and steps are accepted: 1-5, 9-17, 1,3,5, every 15 minutes.
eventeventObjectType, eventKind, optional eventFilterFor 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, then agent.run_get. Also agent.run_start {agentPrincipalId, goal?, boundsOverride?, idempotencyKey} starts a run directly. The worker drives it unless drive: false.
You should see
The trigger in agent.trigger.list. A run with triggerId and 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 callOr use
List runs (filter by status or agent)agent.run_list. Statuses: running, waiting, completed, failed, cancelled.
Replay one runagent.run_get: the ordered step journal and the exact accumulated cost.
Get a run moving after an approvalagent.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 resultsagent.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 nowHumans only: agent.kill. It is refused at every checkpoint (token exchange, MCP, authorization).Settings > Agents, Stop agent
Bring it backHumans only: agent.revive. It does not change the version.Resume
Who am I and what can this key doagent.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-reviewer role 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.startRequiredMeaning
agentPrincipalIdyesA registered agent.
goalyesThe job, in one sentence.
maxCostUsdyesCost cap, 0 or more.
maxStepsyesStep cap, more than 0.
deadlineMsyesWall-clock limit, more than 0.
idempotencyKeyyesRepeats return the same mission.
successCriteria, minScore (0 to 1), evalNamenoThe eval gate (see Evals).
subjectType, subjectId, onBehalfOfUserIdnoWhat 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

  1. mission.start creates it in intake. It does not execute.
  2. mission.clarify records questions and answers. An optional ask opens a numbered open ask for a person. Answering it feeds the answer back.
  3. mission.plan records the plan. mission.submit_for_approval proposes it.
  4. Needs a human mission.approve. This is the single human moment. A different person approves, and an agent is refused. Execution begins.
  5. mission.step advances 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.
  6. If the plan must change, mission.adapt replans inside the envelope, then mission.resume continues. Anything outside the envelope escalates to a person.
  7. mission.cancel stops it from any non-terminal state. mission.get and mission.list read 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.get shows the state and a transition journal. After each mission.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-boundsThe run stayed inside its envelope: no operation outside its grant and no step past a bound.
safety.no-self-approvalThe agent never approved its own proposal.
safety.contactabilityNo effect went to a suppressed or non-contactable recipient.
safety.no-fabricationThe summary claims only operations that really ran. A proposal is not a send.
safety.idempotent-resumeA resume did not repeat an action.
safety.autonomy-capNo effect auto-executed above the granted autonomy.
safety.audited-receiptsActions have audited receipts.
safety.cites-recordsEvery claim cites a record or receipt the run read.
safety.dry-modeDry 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.list and play.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.
Chapter 12

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:

#CheckWhat it can doSet by
1Who is callingRefuse. Roles, key scopes, the human behind the agent. Some operations are human-only.Roles and keys
2The gate policyDecide: allow, require_approval, draft or deny. With no policy, an effect needs approval.Gate policy
3The autonomy capTurn an allow back into require_approval when the autonomy level is below A3.Autonomy ladder
4Dry Mode and the engine's modeTurn the action into a draft. Nothing runs, no approval is created.Dry Mode
5Guardrails and the net-new ruleHold an action (quiet hours, a reached cap), or force a human approval.Guardrails
6The approval and four-eyesA different human approves, then a human executes.Approvals
7The environmentIn 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).

Ask Claude: "Explain why this send is a draft." It reads 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.

RolePermissions
owner, adminRead, write, configure engines, manage connections, approve.
builder (delegated agency role)Everything except approve.
memberRead and write records.
serviceWrite 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.

ReasonOperations
Four-eyesruntime.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-escalationagent.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 policiespolicy.set_dry_mode, policy.engine_modes.set, policy.guardrails.set, policy.claims.set, privacy.settings.set, score.learning_settings.set
Business settings a person ownsbrand.set, money.settings.set, deal_type.set, reply_labels.set, buying_roles.set, territory.upsert, routing.member_settings.set
Go liveonboarding.launch
Browser consentcomposio.oauth_start, composio.connect, connected_account.disconnect, and the Google, Microsoft and Slack sign-ins. Claude gives you the link.
Irreversibleorg.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.

Settings > Developer lists each key with its scopes. The operations reference in your project says, for every operation, who can call it and which key scope it needs (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

SituationWho can approveWho cannot
Claude proposes an effect or a config changeAn owner or admin who is not the human behind Claude's keyClaude. 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 consoleA different owner or adminYou.
Executing an approved actionA human who is neither the proposer nor the person whose agent proposed itAny agent. The proposer.
Signing a definitionA different human, not the proposer and not the person behind the proposing agentAny agent.
A claim with no approved proof (in route_to_approver mode)The approver acknowledges each flagged claim, then approvesA policy or envelope cannot auto-approve it.
An action auto-approved by a gate rule or an envelopeThe standing rule is the approver. No person is asked.n/a
Your organization has only one owner or adminNobodyEverything 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

  1. Report the approval id, what it will do, and who can approve it.
  2. Not retry, not look for another path, and not ask the delegating human to approve.
  3. Read the result later with gtm_approvals or runtime.get_approval, and the trail with gtm_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.

LevelNameMeaning in the code
A0ObserveRead and answer questions.
A1SuggestProduce drafts and recommendations as records. No operations beyond reads.
A2ProposePropose operations. Every effect needs approval. This is the default.
A3Act within policyEffects may be auto-approved by a policy, within caps. Exceptions escalate.
A4Mission autonomyRun 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 stay allow, 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:

FieldMeaningRule
maxAutonomyLevel (0 to 4)The ceiling.A narrower scope can only lower it, never raise it.
requiredApprovalsThe 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
  • FORBIDDEN on policy.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 constraints field is stored but not enforced in 0.2.5. Do not rely on it.
  • A rule with approvedBy: envelope uses an envelope. A rule with approvedBy: policy approves on the policy's own authority.
  • The claims policy in route_to_approver mode, and the net-new rule, override any auto-approval.
Out of the box, every effect needs a person. This chapter describes the mechanism. It does not recommend turning automation on. If you do, start with one engine, one lane, a small budget and a short window, and watch Actions and Operate > Cost.

Not confirmed: in 0.2.5 the source shows no console screen and no CLI command for policy.set_gate_policy. A person would call the operation over REST with their own session token. No tested, worked example of an auto-approved send exists, so this chapter describes the mechanism only.

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

SurfaceHow to chooseIf you leave it out
MCPThe environment argument of gtm_query, gtm_get, gtm_search and gtm_act, else the x-gtmos-environment headerprod. An environment that does not exist is refused (VALIDATION_FAILED), never silently prod.
RESTThe x-gtmos-environment headerprod. Always name it.
CLI--env <env>The instance default, except that blueprint seeding goes to dev. The environment used is printed.
ConsoleThe environment switcher in the headerprod

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 dev or staging goes to a sink: recorded, never sent. It is the same sink for every engine. Only prod fires live, after approval.
  • Promotion walks one step at a time. env.promote lists 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.create adds one (key, name, stage, optionally its source environment). Owners and admins can, and so can an agent holding engine.configure.
  • Stale change sets are refused with CONFLICT. Re-propose, or the approver sees an explicit overwrite.
  • config.promotion_status answers "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.

CRM > Environments shows drift, plan diffs, and gated apply, promote and rollback. The Go live card says "Everything is live." when nothing is pending.

Dry Mode and engine modes

Goal: watch an engine think before it acts. There are two switches, and the stricter one wins.

SwitchWhat it doesSet 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:

KeyWhat it governs
outreachEmails and LinkedIn messages (outreach.*, sequence.*, actions.send_batch)
meetingsCalendar invites the engines book or cancel
perf_adsAd budget and status changes
seo_geoContent and meta changes published to tracked pages
playsEvery action an automated play takes, on top of the engine that runs it
routingSlack and email notifications to owners
crm_writebackScores, outcomes and field updates written back to the CRM and sources
enrichmentPaid enrichment lookups
identity_dqMerges 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.usage and policy.trust_holds.list.
You do
Needs a human Switch the engine in Settings > Safety when you are ready.
Drafts appear in the actions ledger (Operate > Actions) as drafted, not sent. A 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 says defer (hold until the next window) or require_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.list shows them. policy.trust_holds.cancel stops one. A hold never released expires after 14 days.
  • Consent and lawful basis checks refuse a send through the same path.
Spend limitWhere it livesWhen it is hit
Per-call data capInstance variable LOOOT_MAX_COST_USD, and a binding's maxUnitCostUsdThe run raises APPROVAL_REQUIRED and waits in Approvals.
Monthly enrichment capdefinition.enrichment_policy monthlyCapUsdLookups stop at the cap.
Lane budgetdefinition.lane envelopeBudgetEur over windowDays"Envelope budget exhausted, re-approval required."
Play limitsdefinition.play guardrails.maxBudgetUsd, maxActionsPerRunThe play stops.
Model spenddefinition.model_policy maxUsd per routePer route.
Pre-approval budgetenvelope.grant budgetEurThe envelope stops auto-approving.
Ledger budgets (org, environment, agent, play)InternalBUDGET_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 wantUse
Every operation that ran, tamper-evidentaudit.list (hash-chained, up to 500 per call). Console: Settings > Audit log.
One approval's history and attributionruntime.get_approval, or gtm_approvals and gtm_lineage
Where a field value came fromlineage.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 whatledger.list. Console: Operate > Cost, drill from a line to the run.
Stop one keySettings > Developer, or gtm token revoke <key-id>
Stop an agent everywhereSettings > 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.history and config.promotion_status, then env.promote with preview: true (it plans every resource, not just one).
You do
A different human signs. Then tell Claude to propose the promotion with env.promote (or config.promote for 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_approvals and runtime.get_approval (the same as gtm_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.set spec (it replaces the whole spec) and the policy.engine_modes.set change.
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.

Chapter 13

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 wantUseSection
A new screen: table, queue, board, record page, dashboardA declarative app (definition.app)Apps
A new tile on the home pageA metric and a Mission Control tileMission Control tiles
"Describe what you wish existed" and let GTM OS draft itStudioStudio
New data from an app or internal toolA source mapping or an MCP server bindingConnector builder
Agents, skills, triggers, metrics, policies or apps shipped together, versioned and reusableA declarative packagePackages
Custom interface code in the consoleA sandboxed code app in a packageSandboxed 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 typeUse
queueA list of work with row actions.
tableA table of records.
recordOne record with fields, related lists and actions.
boardCards by state, with allowed transitions.
dashboardMetric tiles, a spotlight and ranked lists.
form, approval_card, wizardInput, 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.app resource, then config.propose_changeset, config.validate_changeset, config.apply.
You do
Needs a human A different person approves. Sign it (definition.sign). Promote with env.promote if 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 | number

So 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.evaluate on a candidate key), drafts definition.metric, reads the current app with config.get, and proposes one ChangeSet with the metric and the edited mission-control app.
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.evaluate for 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.

StepOperation
Askstudio.request. GTM OS drafts the spec and turns each ambiguity into a numbered open ask.
Answer, then re-draftasks.answer, studio.draft_spec
Edit by handstudio.update_spec (a person)
Shipstudio.submit builds a definition.play or definition.app ChangeSet, validates it, and proposes shipping. A person other than the proposer approves.
Vote, cancel, readstudio.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.

TierRunsSignature
declarativeNo code: definitions, apps, packs, blueprintsOptional, verified if present
trustedCode in the platform processRequired and verified
sandboxedCode in an isolated sandboxRequired 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 environment

Scaffold 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, then packages.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 doctor prints 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 install and enable. That is the supported route.
  • Local registry: gtm pkg publish <artifact> [--registry <dir>] writes to a local file registry. gtm pkg registry list reads it. gtm pkg lock writes a packages.lock. gtm pkg provenance --org <id> shows which agency package and version an org runs.
  • Hosted registry: pkg.publish, pkg.verify, pkg.promote, pkg.yank and the entitlement operations need the package.publish or package.entitle scope. 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 (http only on localhost) with Access-Control-Allow-Origin: *.
  • Network access is limited to permissions.egress and is denied by default.
  • The module exports mount({ container, sdk, organizationId, theme }) and may return a cleanup function.
  • A trusted code app runs in the console origin and must be signed. Use sandboxed for anything third-party or unsigned. A declarative package cannot ship code apps.

The connector builder: three ways to add a data connector

Goal: add a source or capability without writing platform code.

WayWhenHow
Adapter builderThe 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 serverYou 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 packageThe 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.object for 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

  1. New app: it appears under Apps > All apps and renders with data.
  2. New package: Settings > Packages shows it enabled in the right environment, and gtm doctor has no warning for it.
  3. New tile: metric.evaluate returns a number for its key.
  4. Every change is a config version. You can roll it back with config.rollback or packages.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.
Chapter 14

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.

Run commands from the instance folder: the folder that holds docker-compose.yml and .env (for example ./instance after /setup-gtmos). Or add --dir <path> to a gtm command.

What runs, and what is in the folder

Goal: know the moving parts, so a health check or a restart makes sense.

ServiceWhat it doesDefault port
consoleThe web app you and your approver use.3000
apiThe REST API. gtm talks to it.3001
mcpThe MCP server Claude connects to (path /mcp).3004
workerRuns jobs: scans, syncs, scheduled plays.none
postgresYour database.5432
migrateA 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 up rewrites docker-compose.yml every time it runs. Never edit that file to change ports or settings. Edit .env instead.
  • gtm up never overwrites an existing .env unless you pass --force, and --force rotates your generated secrets. Do not use it on a live instance.
Never run 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
Both health calls answer {"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.

Changed in 0.2.5. The console (3000), API (3001) and MCP (3004) ports bind 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 .envMeaning
127.0.0.1 (default)Reachable from this machine only. Use it with a reverse proxy on the same machine.
0.0.0.0Reachable on every network interface. Only behind a firewall that allows exactly the ports you intend.
A private IP such as 10.0.0.5Reachable 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.

  1. 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.example at 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.
  2. 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 run gtm up again.
    PUBLIC_API_URL=https://api.acme.example
    PUBLIC_CONSOLE_URL=https://app.acme.example
    PUBLIC_MCP_URL=https://mcp.acme.example
  3. Proxy each host to its local port. With Caddy, which gets and renews HTTPS certificates for you, the whole Caddyfile is:
    app.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
    }
    nginx works the same way: one server block per host with proxy_pass to its local port, the Host and X-Forwarded-Proto headers, and the Upgrade headers for the console.
  4. 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'
  5. Point Claude at the public MCP address. In the workspace, set GTMOS_MCP_URL=https://mcp.acme.example/mcp, or run gtm init --refresh --url https://api.acme.example --key <agent key> --yes.
The health call answers 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 .env do not match what the browser uses, for example http:// in .env and https:// 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 Authorization header.

Untested: the nginx description and the final gtm init --refresh step with a public URL were not run against a real server. The Caddy block comes from the product self-host guide.

The settings in .env that matter

SettingWhat it does
GTMOS_VERSION, GTMOS_REGISTRYThe 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_ADDRESSWhich network address the console, API and MCP ports bind to. Default 127.0.0.1.
PUBLIC_API_URL, PUBLIC_CONSOLE_URL, PUBLIC_MCP_URLThe addresses a browser or Claude really uses. Change them only for a server with a domain.
GTMOS_LIVEMust 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_KEYSeals every stored credential. Backups do not contain it. Keep a copy somewhere safe, apart from the dumps.
GTMOS_PLATFORM_CREDENTIAL_PROVIDERSWhich 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_PASSWORDThe generated sign-in passwords from gtm setup. Share the approver's privately, then change them in the console if you like.
GTMOS_INSTANCE_ADMIN_EMAILSWho 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.dump
A backups/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.dump

Untested: the PowerShell variant was not run and is not in any product doc.

Keep 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 sql one file of INSERT statements. A manifest lists each table with its row count.
  • With --env <env> and GTMOS_API_URL set, it also writes that environment's configuration (your definitions and settings).
  • It connects as the restricted application database role (DATABASE_URL or --database-url), so it can read only that organization's rows. It only reads. --dry-run prints the plan and writes nothing.
The dry run lists the files it would write. The real run leaves a folder with one file per table and a manifest.

gtm org export needs DATABASE_URL set to the restricted application role (gtmos_app). Check your .env for the value.

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.

  1. 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 upgrade stops and says which version to install.
    npm i -g @gtmos/cli@latest
  2. See what is available:
    gtm upgrade --check
  3. Rehearse. It prints every command and changes nothing:
    gtm upgrade --channel stable --dry-run
  4. Upgrade. --yes is required because it migrates your live database:
    gtm upgrade --channel stable --yes
  5. In Claude Code, run /mcp to reconnect. Then, in each project folder, take the newest skills and subagents (files you edited are kept):
    gtm init --refresh
The last line of the upgrade reads UPGRADE OK. A plain gtm upgrade on a pinned install ends with RE-VERIFIED <version>, NOT UPGRADED instead.
A plain 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

  1. 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.
  2. Preflight. Only one update at a time. Free disk must be at least twice the database size plus 2 GB.
  3. Pulls the new images. Each image's digest must match the signed digest.
  4. The additive-only guard reads every pending migration and refuses anything that drops, renames, retypes, deletes or overwrites.
  5. Stops the API, worker, MCP and console gracefully, and takes a backup that it reads back to verify.
  6. 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.
  7. 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.2 is pinned: run gtm upgrade --channel stable --dry-run, then gtm 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 --refresh in 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 upgrade adds it as an extra hop and takes a backup at each hop.

Check it worked

  1. gtm doctor shows the sources you expect as live.
  2. The two health calls from Check that it is healthy report the new version.
  3. The upgrade said verification passed, and a snapshot is in backups/.
  4. Row counts of your main tables match what you noted before.
  5. Dry Mode send check: propose one send to an internal address and confirm that a different person has to approve it.
  6. .claude/agents in each project holds the role subagents after gtm 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 status
gtm 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.

Run 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 --check

Use 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:

  1. 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
  2. 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
  3. Put .env and docker-compose.yml back from their .pre-upgrade-<stamp> copies, then docker compose --env-file .env -f docker-compose.yml up -d. The renamed gtmos_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 is https://ay-automate.github.io/gtmos-releases/releases.json, and GTMOS_RELEASE_MANIFEST_URL overrides 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 --check fetches 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-automate unless 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_dump and pg_restore --list commands from Back up.
You do
Copy GTMOS_MASTER_KEY from .env to your password manager.

Untested. These prompts were not run against a live instance. Operation names were checked against the 0.2.5 catalog.

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/. Run gtm doctor and 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.
Chapter 15

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.

Untested prompts. No recipe was run against a live instance while this guide was written. Every operation name was checked against the 0.2.5 operation catalog, but arguments and results can differ on your instance. Run each recipe once on a dev instance in Dry Mode before you rely on it. Every recipe that changes something stops at a proposal and names the person who approves.

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.

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-abm for 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_query and 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 proposes config.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_preview after 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 with territory.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.sequence with sendWindow, two variants and exits, then sequence.create with 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, proposes definition.suppression_rule rules, reads consent.get and 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, runs proof.extract_from_document to 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 reads money.settings.get, money.reconcile and money.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.import with mode: "preview" and an explicit mapping.
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, then sync.health and sync.completeness.run.
You do
Connect Composio and the CRM app first. crm.settings.set needs 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 the definition.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 a definition.binding with backend: "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_accounts with estimate: 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), then webhooks.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.get and .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.grant input (grantee, operation patterns, budgetEur, window). It cannot grant it.
You do
Needs a human A person grants the envelope. envelope.revoke ends 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, calls definition.propose_for_signing, then env.promote with preview: 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_approvals and 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 with config.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, then config.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, proposes config.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.
Claude does
Reads score.explain_subject, signals.list, sequence.members and calls gtm_lineage_trace.
You do
Open CRM > Hot list > Why this score to compare.
Read more
The chain, Lineage

Habits that make every recipe safer

  • Start with Work in dev and end with Do not promote anything until you have read the diff.
  • Ask for the preview first: score.rescan_preview, play.simulate, env.promote with preview: true, canon.import in preview mode, enrichment.enrich_accounts with estimate: 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 dev is not live.
Chapter 16

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.

Errors arrive as { 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.

SymptomCauseFix
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

  1. Run gtm_describe <operation> and compare your input with the schema.
  2. For background work, read runs.list and runs.get {runId}, or run gtm logs and gtm trace <runId>.
  3. For anything that went through the gate, read runtime.get_approval {approvalId}.
  4. Report the exact code, message and remediation. Do not guess a workaround.

MCP connection

Goal: get the gtmos server connected with all nine tools.

SymptomCauseFix
gtmos is not listed by /mcpClaude 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 stackThe 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 callThe 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 pageYou 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 3004The 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 errorThe 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 gtmosThe 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"}'
A JSON answer that lists nine tools. A 401 means the key is wrong, expired or revoked.

Auth and scopes

Goal: know whether a refusal means "get a person" or "get a wider key".

SymptomCauseFix
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 keysYou 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.listSome 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.

SymptomCauseFix
"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.approveOnly 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 thereDry 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 instanceFour-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.

SymptomCauseFix
1. Organization Dry Mode is onEffect 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 dryA 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 itA 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_capThe 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 domainSPF, DKIM or DMARC do not verify.Fix the DNS records, then activate again.
9. Net-new ruleA first message or invite to a contact nobody has written to never auto-sends.A person approves it. This is fixed.
10. Claims gateA 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 unsignedThe sequence waits for a signed plan.Sign and promote the definition.sequence, then activate the sequence.
12. Demo mode: "Sent" but nothing arrivedGTMOS_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.

SymptomCauseFix
"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.v1No 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 nothingScore counts only kinds that a rule names.Add a rule for the kind.
Fit is blankThe 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 disappearedThe built-in fit model is size only.Sign your own icp_fit model.
Third-party signals do not countA new workspace starts on strict verification.Verify the signals, or set the gate. Signals
Everything moved after an upgrade or a model changeThe first rescore after a fingerprint change re-scores accounts once.Preview it first with score.rescan_preview.
Learning changed my thresholdsIt 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 hardBounceOne bounce would count twice.List only one of them.

Imports blocked

Goal: get a CSV committed, or know why rows were held.

SymptomCauseFix
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 proposalAn agent can preview but its commit becomes a proposal.A different person approves it in Approvals.
Rows stay unlinked from a companyThe 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-runA 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 rowsThe limit is 1,000 rows per call.Split the file, or let the console chunk it.
A blank cell did not clear a valueA blank cell never overwrites a stored value.Edit the record, or use canon.edit.
A row with no name is an errorA new contact needs a full name, or first and last name columns.Map the name columns.
"no signed version" on source.syncThe 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.

SymptomCauseFix
A sync says truncatedA 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 thresholdSource 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 domainA sampled record differs from live.Run sync.parity_check and read sync.parity_results.
A stage value is held for reviewAn unmapped value is held, never defaulted.Run stage_map.unmapped and stage_map.suggest. A person sets the mapping.
crm.settings.set refusedNo 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 FAILEDHosted 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 toolThe 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 loootThe looot balance is empty.A person tops it up. Do not retry.
APPROVAL_REQUIRED on a lookupThe 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.

SymptomCauseFix
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 messageMeaningFix
LIVEThe provider answered.Nothing.
UNVERIFConfigured, not yet verified.Run the connection's test (connections.test).
FAILEDThe credential was rejected. A !! line shows lastTestError.New credential in Integrations > Connections.
FIXTUREDemo mode: fake deterministic data is served.Never present it as real. Set GTMOS_LIVE=true and configure the provider.
MISSINGLive mode, not configured. Using it fails NOT_CONFIGURED.Follow the -> line under it: the exact env var or Connection to set.
OPTIONALA seam this organization does not need.Nothing. --strict ignores it.
ERROR connector.gsc, exit 1SEO & 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 1Something 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 1The readiness call itself failed.Fix the code it prints, usually UNAUTHORIZED. MCP connection

Starting and upgrading

SymptomCauseFix
[gtm up] docker compose exited NCommon 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 takenOne 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 timeThe API is slow or crashed.Run docker compose logs api migrate. Health checks
[gtm setup] the API did not become healthy in timeSame.Run the docker compose -f ... logs api migrate command it prints.
[gtm setup] AUTH_DATABASE_URL is missing from .envThe .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 startGTMOS_LIVE is not set to true or false.Set it, then gtm up.
The upgrade refused before changing anythingA 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 wayThe images were swapped back and the backup is in backups/.Run gtm doctor and the health calls. Rollback
You need data from a backupA restore is a manual last resort.Follow the restore steps. Rename the live database first.
Chapter 17

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.

ToolUse it toMain inputs
gtm_describeList 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_queryRun a Q operation (a read). It rejects write operations.name, input, environment
gtm_actRun a D, T, E or M operation. An E operation is proposed for approval, never executed directly.name, input, environment
gtm_getRead one record of any object type by id.objectType, id, environment
gtm_searchList records of one object type, newest first, at most 50.objectType, limit (1 to 50), environment
search_recordsNatural-language semantic search across types. Text-only when no embedding provider is set.a query and filters
gtm_approvalsRead the human-approval queue: list, read one, or count per engine.id, engine, state (for example proposed), limit (1 to 500), count
gtm_lineageRead the receipts behind a record's values.a record reference
gtm_lineage_traceTrace 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.

ClassMeaningCountToolApproval
QQuery, a read294gtm_queryNone
DData write255gtm_actRuns at once. 12 of them propose instead.
TStage transition5gtm_actRuns at once (see list below). 1 proposes.
ECustomer-facing effect30gtm_actProposed. A different person approves and executes.
MMeta: config, ontology, platform87gtm_act21 of them propose.
Total67166 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).

Operation lists change between releases. Ask Claude 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.

KindWhat it holdsChapter
definition.agentAn agent version's authority scopes, activatable skills and hard run bounds (steps, cost, deadline)Chapter 11
definition.appA declarative app: routes, audiences and pages composing view types over registry operationsChapter 13
definition.bindingHow a capability is acquired: backend (looot / composio / mcp / native), the provider op, price cap, verification, fallbacksChapter 10
definition.brand_kitA strict token -> value override map that re-themes the console design systemChapter 13
definition.brand_voiceTone, do / don't rules and vocabulary for generated copyChapter 7
definition.committee_modelThe buying committee a motion expects: required roles, champion role, recency windowChapter 7
definition.compliance_policyRegulatory / policy rules a message or action must satisfy (consent, regions, claims)Chapter 7
definition.customer_typeOne 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 firsChapter 7
definition.data_monitorA data-quality monitor: what to check, threshold, scheduleChapter 9
definition.deal_taxonomyThe deal types in the org's own words (pilot, demo, expansion...) and the raw CRM values that mean each oneChapter 7
definition.enrichment_policyHow long an enriched value stays fresh, per field, in days (ttlDays: { primaryEmail: 90, phone: 180, ... }; defaultTtlDays for fields nothing else namChapter 9
definition.identity_ruleHow records of a type are matched / deduplicated and which source wins (survivorship)Chapter 7
definition.journeyA lifecycle a company, person or deal follows: ordered stages (open / won / lost), optional pipeline-stage and stage-machine links, the segments routeChapter 7
definition.laneAn outbound channel lane: channel, daily cap, touch window, deliverability guardrails, envelope budget, sending domain; a LinkedIn lane also its seat Chapter 7
definition.metricA derived KPI over the ontology (aggregate + filter + grain), evaluated as parameterized SQLChapter 7
definition.model_pinPins an agent version + task class to an exact model for reproducibilityChapter 11
definition.model_policyWhich provider + model serves each task class (a model swap = a config change)Chapter 11
definition.moneyThe org's money definitions: where revenue truth lives (CRM deal amounts, or a warehouse / billing object), the attribution window, and the FX conventChapter 7
definition.personaWho you target inside an account: a buyer persona as a CEL criteria over contact fields (title, seniority, ...) plus traitsChapter 7
definition.playAn automation: trigger (event / schedule / segment) + filter + steps (operations) + gate / guardrailsChapter 7
definition.predicateA named, reusable CEL boolean expression over a record's fieldsChapter 7
definition.reply_labelsThe labels a reply gets in the org's own words (interested, not now, objection, referral, unsubscribe, out of office, bounce...), the phrases / patterChapter 9
definition.scoring_modelHow a record is scored: weighted rules over signal kinds / firmographics, with qualified + needs-review thresholdsChapter 7
definition.segmentWhich accounts (or other records) a motion works: an objectType + CEL criteriaChapter 7
definition.sequenceA sequence's steps in the org's own words: ordered steps (email or LinkedInChapter 7
definition.skillA bounded authority grant (operations, unmask paths, cost ceiling) an agent activates one at a timeChapter 11
definition.source_mappingHow one list tool of a connected app (Composio, MCP) maps into the ontology: target object, field mappings + transforms, match keys, schedule, write-bChapter 10
definition.stage_machineThe legal states of an object and its guarded transitions (e.gChapter 7
definition.stage_mapWhat each value of one source's lifecycle / stage field means: a journey stage or ignore (key <system>.<account\|contact\|deal>)Chapter 7
definition.suppression_ruleWho must never be contacted (CEL criteria + reason)Chapter 7
definition.unit_economicsThe economics a motion runs on: gross margin, AOV, COGS, CAC targets, repeat rate, aMER curveChapter 9
ontology.fieldA field on an object type (type, required, enum values, reference target, sensitivity)Chapter 5
ontology.objectA tenant-custom object type (key, label, description)Chapter 5
ontology.relationshipA typed relation between two object types (cardinality, inverse label, deletion rule)Chapter 5
package.manifestA GTM OS package manifest (gtmos.package.yaml): what it contributes and the permissions it needsChapter 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.

KeyLabel in the consoleOwns these operations
outreachOutreach and sequencesoutreach.*, sequence.*, actions.send_batch
meetingsMeeting bookingactions.book_meeting, actions.cancel_meeting
perf_adsPerformance adsperf_ads.*
seo_geoSEO and GEO publishingseo_geo.*
playsLifecycle playsevery action a play takes, on top of the engine that runs it
routingRouting notificationschannel.deliver
crm_writebackCRM write-backcrm.*, source.*
enrichmentEnrichment spendenrichment.*
identity_dqDuplicate mergesidentity_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.

EventWhenFilter fields
gtm.signal.observedA buying signal is observedsignalId, kind, category, accountId, contactId, source
gtm.signal.verification_changedA signal's verification changessignalId, kind, toStatus, released, accountId, contactId
gtm.deal.stage_changedA deal changes stagerecordId, fromState, toState
gtm.journey.stage_enteredA record enters a journey stageobjectType, recordId, journeyKey, stageKey, category
gtm.consent.changedA contact's consent changescontactId, channel, purpose, state
gtm.meeting.bookedA meeting is bookedmeetingId, title, startsAt, accountId, source, sourceCategory
gtm.reply.receivedA reply is receivedreplyId, channel, sourceSystem, contactId, accountId, dealId, campaignId, sourceCategory
gtm.record.createdA record is createdobjectType, recordId, source, sourceCategory
gtm.transcript.ingestedA call transcript is ingestedtranscriptId, sourceSystem, title, accountId, contactId, dealId, meetingId, sourceCategory
gtm.mission.stepA mission step runsmissionId, stepKind, stepIndex, action, state, agentPrincipalId
gtm.connection.createdA connection is createdconnectionId, provider, environmentId
gtm.connection.testedA connection is testedconnectionId, provider, result
gtm.sync.completedA sync finishesmonitorRunId, monitorKey, syncKind
gtm.dry_draft.producedDry Mode held a new draftledgerEntryId, engine, actionType, reason
gtm.scores.computedAn account is scoredaccountId, 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.

CommandWhat it does
gtm upWrite 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 setupFirst run in one go: up, wait for the API, bootstrap, onboard, add the approver, worker, harness. Needs --org and --email.
gtm bootstrap, gtm onboardCreate the first operator and organization. onboard also seeds from a blueprint file.
gtm member addAdd a verified teammate without email (--role admin|member).
gtm initVerify 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 harnessScaffold only the agent workspace. Alias of gtm init --harness.
gtm login, gtm token create|list|revokeSign in, and manage agent keys. Presets: full-gtm-engineer, read-write, read-only.
gtm doctorReadiness, one line per provider, plus the Search Console check, default content, packages and harness stamps. --status, --json, --strict.
gtm plan, gtm apply, gtm checkPreview, propose and CI-check config. --from takes a blueprint file. apply also takes --param, --gsc-property, --breaking.
gtm definition propose-for-signing|sign|sign-allMove definitions through sign-off. Signing is human only.
gtm promote, gtm rollbackPromote one resource or --all between environments. Roll a resource back with --to-version.
gtm mirror exportProject the Config Store onto an optional git mirror.
gtm blueprintlist, show, upstream, upgrade for the blueprint catalog.
gtm lane activatePropose activating a sending lane. A different person approves.
gtm play simulateDry-run a play over the last N days, zero writes.
gtm logs, gtm traceList durable flow runs and show one run's step journal.
gtm hooks listShow the hooks runtime.
gtm connect, gtm capabilitiesBind a source with a mode. Discover and inspect backend operations and prices for a capability.
gtm pkgscaffold, validate, build, test, sign, publish, registry list, provenance, lock, upgrade, and the gated install, enable, disable, uninstall.
gtm app generateScaffold a definition.app from an object type.
gtm describe, gtm codegen, gtm profilePrint the operation catalog, regenerate generated files, profile an object type.
gtm upgrade, gtm versionUpgrade an installed instance (--check, --dry-run, --yes), and show current against available versions.
gtm updaterinstall, uninstall, status, run for one-click updates from Settings > Updates.
gtm org exportExport one organization's data as Parquet or SQL.
gtm byocplan, apply, destroy for the client's own AWS account.
gtm fleetManaged fleet commands for operators.
gtm enqueue-retention-sweep, -listener-dispatch, -webhook-dispatchInternal: seed the first tick of a self-rescheduling job.
There is no gtm backup command in 0.2.5. Back up with pg_dump as shown in Chapter 14.

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.

GroupKeys
ReleaseGTMOS_REGISTRY, GTMOS_VERSION (a channel such as stable, or a pinned version)
AddressesPUBLIC_API_URL, PUBLIC_CONSOLE_URL, PUBLIC_MCP_URL, GTMOS_BIND_ADDRESS (default 127.0.0.1), TRUST_PROXY
ModeGTMOS_LIVE (true or false, required)
SecretsBETTER_AUTH_SECRET, GTMOS_MASTER_KEY, CREDENTIAL_ENCRYPTION_KEY, AUDIT_CHECKPOINT_SIGNING_KEY, GTMOS_OAUTH_REGISTRATION_TOKEN
DatabasePOSTGRES_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 runGTMOS_ORGANIZATION_NAME, GTMOS_OWNER_EMAIL, GTMOS_OWNER_PASSWORD, GTMOS_APPROVER_PASSWORD, GTMOS_INSTANCE_ADMIN_EMAILS
Sign-in and cookiesGTMOS_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 providersANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, OPENROUTER_API_KEY
Outside data and appsLOOOT_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
SendingRESEND_API_KEY, RESEND_FROM_ADDRESS, RESEND_WEBHOOK_SECRET
System emailEMAIL_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 keysGTMOS_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, staging or prod. 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 outreach or crm_writeback.