Start here

Connect an agent in production to Contro1

Connect your own agent (LangGraph, Mastra, OpenAI Agents, CrewAI or plain HTTP) with an Agent Credential, give it Actions in Access, and call them.

Updated Oct 21, 2026

An agent in your own code gets its own identity and an Agent Credential, a key bound to that one agent. You give it Actions in Access, and it calls them with the SDK or plain HTTP; every call is attributed to it and checked by Contro1.

Who this is for

Your own code, running unattended: a backend, a worker or a scheduled job. For: LangGraph, Mastra, OpenAI Agents SDK, CrewAI, Pydantic AI, Strands, n8n. Acts as: The agent itself. Credential: An Agent Credential in your secret manager.

An agent in your own code gets its own identity and an Agent Credential, a key bound to that one agent. You give it Actions in Access, and it calls them with the SDK or plain HTTP; every call is attributed to it and checked by Contro1.

All four ways to connect an agent

Connect it

Three things have to exist before the first call: the agent, the application access it was given, and its credential. Then one call proves it works.

StepWhat to doWhat you should see
Create the agentcontro1 auth login contro1 init --name "Invoice agent" --framework langgraphThe agent appears in Agents with you as its owner. contro1 init prints "Connected and set agt_... as the default agent for profile default" and a table with its agent_id.
Give it the Actions it needsIf the application is not on yet, an administrator turns it on under Apps and chooses which of its Actions the organization allows. Then open Access, choose the agent, connect the account it should use (for example the team mailbox), tick the Actions it may run, for example gmail.message.list, and press Save.On the agent's page, the "Get it working" card shows "Can do something" as done.
Create its Agent CredentialOpen Settings, Agent keys, press Add agent key, choose the agent, name the place it will run (for example "Production server") and press Create Agent Credential. The New agent guide offers the same button right after Create identity.The key is shown once. Copy it straight into your secret manager. Agent keys then lists it under the name you gave.
Call an Action from your code# 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')}")HTTP 201 with "state": "executed" and a "result". Or 202 with "state": "awaiting_approval" while a person decides: poll GET /api/centcom/v1/actions/{invocation_id} until the state is terminal, then read "result".
Confirm it in Contro1Open the agent in Contro1.The call is in its activity under the agent's own name, and the "Get it working" card is gone, because all three of its steps are done.

Connect it, step 1: Create the agent

In Contro1, open Agents, press New agent and choose "An agent you are building for production". Give it a name and press Create identity. You are its accountable owner, and it starts with no access and no credential.

Or from a terminal, with the CLI installed: sign in, then register it. A coding agent connected to Contro1 over MCP can also do this with the register_agent tool.

terminal
contro1 auth login
contro1 init --name "Invoice agent" --framework langgraph

What you should see: The agent appears in Agents with you as its owner. contro1 init prints "Connected and set agt_... as the default agent for profile default" and a table with its agent_id.

Connect it, step 2: Give it the Actions it needs

If the application is not on yet, an administrator turns it on under Apps and chooses which of its Actions the organization allows. Then open Access, choose the agent, connect the account it should use (for example the team mailbox), tick the Actions it may run, for example gmail.message.list, and press Save.

What you should see: On the agent's page, the "Get it working" card shows "Can do something" as done.

Connect it, step 3: Create its Agent Credential

Open Settings, Agent keys, press Add agent key, choose the agent, name the place it will run (for example "Production server") and press Create Agent Credential. The New agent guide offers the same button right after Create identity.

Store it as CONTRO1_API_KEY in the secret manager of the place it runs. Never in code, a prompt or a tool argument.

What you should see: The key is shown once. Copy it straight into your secret manager. Agent keys then lists it under the name you gave.

Connect it, step 4: Call an Action from your code

authority_mode "agent_principal" means the agent acts as itself. account_mode "shared" names a shared account you gave it. Send an Idempotency-Key on anything with side effects, and never re-submit while a person decides.

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

What you should see: HTTP 201 with "state": "executed" and a "result". Or 202 with "state": "awaiting_approval" while a person decides: poll GET /api/centcom/v1/actions/{invocation_id} until the state is terminal, then read "result".

Connect it, step 5: Confirm it in Contro1

Open the agent in Contro1.

What you should see: The call is in its activity under the agent's own name, and the "Get it working" card is gone, because all three of its steps are done.

The same call inside your framework

Each guide shows this call as a tool in that framework's own idiom, plus its trace hook, so every tool call is reported from the runtime with one trace_id per run.

LangGraph · Mastra · OpenAI Agents SDK · CrewAI · Pydantic AI · Strands Agents · n8n · Claude Managed Agents · Python SDK · JavaScript and TypeScript SDK

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 let this agent act for them with that Action.

When the account's owner is also the agent's owner and gives it the Action in Access, that counts as letting it act for them. In every other case the account holder opens the agent in Access and turns it on themselves. 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()),
)

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.
  • When a named part of this agent makes the call (a worker, a crew member, a subagent), send the header Contro1-Sub-Agent: <name> with this agent's credential. Contro1 registers the part the first time; it never has more authority than this agent.
  • Report every tool call from a hook in the runtime, with one trace_id per run, rather than asking the model to report. See /docs/agent-traceability-and-evidence.
  • 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_ENABLEDThis computer connection is Approvals only (set by its owner or a setup policy).Open the agent in Access and press "Allow applications", or open the page contro1 apps enable prints.
An Action is missing from list_actionsNobody gave it to this agent, or the app is not turned on in Apps.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 let it use this Action on their account.That person opens the agent in Access and turns it on. The refusal names them and links the page.
"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.Sign in again with "Run Actions as you" ticked (contro1 mcp login --execute for a local server).
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

Can I connect a framework agent over MCP instead?

Yes, if the framework has an MCP client that can complete the device sign-in (Contro1 refuses sign-ins that return to 127.0.0.1). It then acts as the person who approved it, not as its own agent. For an agent that runs unattended in production, an Agent Credential gives it its own identity and permissions.

My system has several agents in one process. Do I connect each one?

No. Connect the system once, as one agent. Each part says who it is with the Contro1-Sub-Agent header (client.as_sub_agent in the SDKs) and appears under it on the org map, marked ! until an administrator approves it, with its own trail. A part runs on the agent's grants and can be narrowed or blocked on its own. Connect a part separately only when it runs as its own program somewhere else, and link it with contro1 init --parent.

Where does the credential live?

In your secret manager, read from the environment as CONTRO1_API_KEY. Never in code, a skill, a prompt or a tool argument.

Related resources

Contro1 Action Gateway

Choose the applications your organization approves, decide who may do what, and connect one MCP. Every person and agent gets exactly their access, and every action is recorded on their name.

Contro1 Python SDK

Install and use the framework-agnostic Contro1 Python SDK to request human approvals or input, verify signed webhooks, log autonomous actions, and fetch audit evidence.

Contro1 JavaScript and TypeScript SDK

Install and use @contro1/sdk in Node, TypeScript, Express, Fastify, or Next.js backends to create approval requests, verify webhooks, and write audit records.