Start here

Connect an agent to Contro1

Three ways to connect an agent to Contro1, chosen by where the agent runs: in your own code, in an app with an MCP connector, or on a computer. Exact steps and request shapes for each.

Pick the path by where your agent runs. Each one ends with the agent able to ask people for approval and run the application Actions you gave it, with every call checked and recorded.

Key takeaways

  • Agent in your own code (LangGraph, Mastra, OpenAI Agents SDK, CrewAI, Pydantic AI, Strands, n8n): an Agent Credential and the SDK or plain HTTP.
  • App with an MCP connector (Grok, Claude, ChatGPT, Cursor, Codex): add https://api.contro1.com/api/centcom/mcp and approve. It acts as you.
  • Agent on a computer (Claude Code, OpenClaw, NanoClaw): contro1 connect. No key is stored where the agent can read it.
  • Connecting grants nothing. An agent can use an application only after you give it Actions under Access.

Pick your path

The question is where the agent runs, not which framework it uses. That decides how it can hold a credential, and so who it acts as.

Where the agent runsExamplesHow it connectsIt acts as
Your own code, on your serversLangGraph, Mastra, OpenAI Agents SDK, CrewAI, Pydantic AI, Strands, n8n, customPath A: Agent Credential + SDK or HTTPThe agent itself
An app with an MCP connectorGrok, Claude.ai, Claude Desktop, ChatGPT, Cursor, CodexPath B: MCP URL + approval in the browserYou, the person who approved
A computer you controlClaude Code, OpenClaw, NanoClawPath C: contro1 connectThe agent itself, with a key that never leaves the machine

Before any path: what Contro1 needs to exist

  • The agent, as an identity with an accountable owner. Paths B and C create it for you when you approve. For path A, create it with contro1 init or let your coding agent call the MCP tool register_agent.
  • The application account it will use, connected once under Apps (for example the team mailbox in Gmail).
  • Access: the specific Actions the agent may run on that account, given on the Access page or in the capsule on the agent page. Nothing else authorizes an Action.

Path A: an agent in your own code

For an agent that runs in your backend, a worker or a scheduled job. It authenticates with an Agent Credential: a key bound to one agent, so every call is attributed to that agent and nothing in a request body can change which agent it is.

  • 1. Register the agent: contro1 init --name "Invoice agent" --framework langgraph (or ask your coding agent to call register_agent over MCP). Finish the setup link it prints: purpose and owner.
  • 2. Connect the application account under Apps, if it is not connected yet.
  • 3. Give the agent the Actions it needs under Access (for example gmail.message.list on the team mailbox).
  • 4. Create its credential: Settings, API keys, Create Agent Credential, choose the agent. Store it as CONTRO1_API_KEY in your secret manager. Never in code, a prompt or a tool argument.
  • 5. Call the Action from your code, as below.
run_action.py
# pip install "centcom>=1.5.0"
import os, uuid
from centcom import CentcomClient, needs_human_resolution

client = CentcomClient(api_key=os.environ["CONTRO1_API_KEY"])

out = client.actions.invoke(
    "gmail.message.list",
    {"max_results": 5},
    authority_mode="agent_principal",   # the agent acts as itself
    account_mode="shared",              # the team mailbox you granted
    connection_id="acn_...",            # optional when only one account fits
    idempotency_key=str(uuid.uuid4()),  # required for anything with side effects
)
invocation = out["invocation"]

if invocation["state"] == "executed":
    messages = out["result"]
elif invocation["state"] == "awaiting_approval":
    settled = client.actions.wait_for_invocation(invocation["invocation_id"])
    if needs_human_resolution(settled):
        raise RuntimeError("Outcome unknown: a person must check. Do not retry.")
    messages = client.actions.get_result(settled["invocation_id"])
else:
    raise RuntimeError(f"Not run: {invocation['state']} {out.get('not_run')}")
runAction.ts
// npm install @contro1/sdk@^1.5.0
import { CentcomClient, ActionsApi, needsHumanResolution } from "@contro1/sdk";

const actions = new ActionsApi(new CentcomClient({ apiKey: process.env.CONTRO1_API_KEY! }));

const out = await actions.invoke({
  action_id: "gmail.message.list",
  input: { max_results: 5 },
  authority_mode: "agent_principal",
  account_mode: "shared",
  idempotency_key: crypto.randomUUID(),
});

let messages: unknown;
if (out.invocation.state === "executed") {
  messages = out.result;
} else if (out.invocation.state === "awaiting_approval") {
  const settled = await actions.waitForInvocation(out.invocation.invocation_id);
  if (needsHumanResolution(settled)) throw new Error("Outcome unknown: a person must check. Do not retry.");
  messages = await actions.getResult(settled.invocation_id);
} else {
  throw new Error(`Not run: ${out.invocation.state}`);
}
terminal
curl -X POST https://api.contro1.com/api/centcom/v1/actions/invoke \
  -H "Authorization: Bearer $CONTRO1_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "action_id": "gmail.message.list",
    "input": { "max_results": 5 },
    "authority_mode": "agent_principal",
    "account_mode": "shared"
  }'

# 201: ran. The body has "invocation" and, when it ran now, "result".
# 202: waiting for a person. Poll:
curl https://api.contro1.com/api/centcom/v1/actions/<invocation_id> \
  -H "Authorization: Bearer $CONTRO1_API_KEY"
# Once "state" is "executed", the same response carries "result".

Path A, on a person's own account

A personal account, such as somebody's own Gmail, is always used on behalf of the person it belongs to. The agent names them: authority_mode "user_delegated" and acting_user_id set to that person. Contro1 accepts it only if that person delegated the Action to this agent.

When the owner of the account is also the agent's owner and gives it the Action in the capsule, that is recorded as their delegation. The agent's whoami lists who it may act for under acts_for.

On the owner's personal account
client.actions.invoke(
    "gmail.message.list",
    {"max_results": 5},
    authority_mode="user_delegated",
    acting_user_id="<the person's id, from whoami acts_for>",
    account_mode="personal",
    idempotency_key=str(uuid.uuid4()),
)

Path B: an app with an MCP connector

For Grok, Claude.ai, Claude Desktop, ChatGPT, Cursor, Codex and any client that can add a remote MCP server. The client connects as you: it can do what you can do and nothing more, and every call is recorded as yours.

  • 1. In the client, add a custom connector or MCP server with the URL https://api.contro1.com/api/centcom/mcp.
  • 2. The client opens a Contro1 approval page. Sign in and review what it asks for.
  • 3. To let it run Actions, tick "Run Actions as you". It is off by default; without it the connection can read and preview only.
  • 4. Ask it something that needs an application. It calls list_actions, then invoke_action.
MCP server address
https://api.contro1.com/api/centcom/mcp

Path C: an agent on a computer

For Claude Code, OpenClaw and NanoClaw. A Contro1 service on the computer holds the agent's key, which cannot be exported, and the agent talks to it over a private local endpoint. The agent never holds a credential, so it cannot leak one.

  • 1. Install the CLI. macOS and Linux: curl -fsSL https://contro1.com/install.sh | sh. Windows: irm https://contro1.com/install.ps1 | iex.
  • 2. contro1 auth login, and approve in the browser.
  • 3. contro1 connect claude-code (or openclaw, nanoclaw). It shows what will change, then run it again with --yes. On Windows it asks once for administrator approval.
  • 4. Open the approval link it prints and approve the agent. You are its accountable owner.
  • 5. contro1 doctor claude-code. Every line should be green.
  • 6. To let it use applications: on the agent page, turn on "Let it use applications", give it Actions in the capsule, then run contro1 apps enable claude-code once on the computer.
Claude Code: add the Contro1 MCP server
claude mcp add --scope user contro1 -- contro1 mcp serve
# If Claude Code cannot find contro1, give the full path, for example on Windows:
# claude mcp add --scope user contro1 -- "%LOCALAPPDATA%\Programs\contro1\contro1.exe" mcp serve

Ask a person before a step you run yourself

Actions are for work Contro1 performs with an account it holds. For work your own code performs, such as a database migration or a payment through your own client, ask for a decision first and continue only when it is approved. It uses the same Agent Credential.

Requests API ยท Webhooks

For coding agents: the exact contract

If you are an AI coding agent wiring an application to Contro1, these are the rules the server enforces. Follow them exactly.

  • Identity comes from the credential. Never send agent_id in a request body.
  • Invoke: POST https://api.contro1.com/api/centcom/v1/actions/invoke with Authorization: Bearer <Agent Credential>. Body fields: action_id, input, authority_mode, account_mode, and optionally connection_id, acting_user_id, action_version, context, target_resource, trace_id, parent_trace_id, thread_id, callback_url. Unknown fields are refused.
  • Send an Idempotency-Key header on every side-effecting Action, and reuse the same key only when retrying the same intent. Never derive it from the payload.
  • Read: GET https://api.contro1.com/api/centcom/v1/actions/{invocation_id}. The response has invocation and, once executed, result. Poll it; never re-submit to find out what happened.
  • awaiting_approval means a person decides. Poll until the state is terminal: executed, execution_failed, execution_indeterminate, denied, expired, cancelled or binding_mismatch.
  • execution_indeterminate means the outcome is unknown. Stop and tell a person. Retrying can send a second email.
  • Before writing code, call get_action_contract (MCP) for the Action: it returns the exact input_schema.
  • Credentials come from the environment or a secret manager. Never put one in code, a skill, a prompt or MCP output.

When a call is refused

What you seeWhat it meansWhat to do
APPLICATION_ACTIONS_NOT_ENABLEDThe connection is set to approvals only.On the agent page, turn on "Let it use applications".
An Action is missing from list_actionsNobody gave it to this agent.Give it under Access, or call request_capability to ask the account owner.
DELEGATION_NOT_FOUNDThe agent tried to act for a person who has not delegated this Action to it.That person gives it in the agent's capsule on their own account.
"A personal account is used on behalf of the person it belongs to"A personal account was called without naming the person.Send authority_mode "user_delegated" with acting_user_id.
EXECUTE_SCOPE_REQUIRED (MCP)This MCP connection was approved to read and preview only.Reconnect and tick "Run Actions as you".
SCOPE_UPGRADE_REQUIREDAn old organization key without scopes.Create an Agent Credential for the agent.
awaiting_approvalA person has to decide.Poll the invocation. Do not submit it again.
execution_indeterminateThe provider may or may not have done it.Stop. A person checks the provider and decides.

Frequently asked questions

Which path should a LangGraph or Mastra agent use?

Path A: register the agent, give it Actions, create an Agent Credential, and call the Action with the SDK or HTTP. The framework guides show the same call inside a LangGraph tool or a Mastra tool.

Can I connect a framework agent over MCP instead?

Yes, if the framework has an MCP client that can complete the OAuth approval (or the device grant for a headless client). It then acts as the person who approved it, not as its own agent. For an agent that runs unattended in production, path A gives it its own identity and permissions.

Does connecting give the agent access to my applications?

No. Connecting establishes who the agent is and who answers for it. Access to an application comes only from Actions somebody who may give access to that account grants it.

Where does the agent's credential live?

Path A: in your secret manager, read from the environment. Path B: in the MCP client, as an OAuth token. Path C: in the Contro1 service on the computer, as a key that cannot be exported; the agent never sees it.

Related resources

Contro1 Action Gateway

Let Contro1 hold the provider credential and make the call, so the record of what happened is what Contro1 observed rather than what your agent reported.

Contro1 CLI

Install and use the contro1 CLI to register AI agents, create approval requests, push AI inventory, retrieve evidence and traces, and test workflows before using the SDK or API.