Agents

Manage agents inside a company.

Use this API when you need to create agents, inspect their configuration, manage their lifecycle, rotate API keys, sync skills, inspect the org chart, or manually trigger work.

Quick Rules

  • This API is company-scoped. Every meaningful request is tied to a company.
  • Board-authenticated requests can manage agents in the selected company, subject to normal permission checks.
  • Agent-authenticated requests can only act inside their own company.
  • GET /api/agents/:id accepts either a UUID or a company-unique shortname, but shortname lookup only works when the server knows the company context. In practice that means ?companyId=... or agent auth.
  • Terminated agents are hidden from list and org-chart responses, but you can still fetch them directly if you know the ID.
  • GET /api/agents/:id may return a redacted view for another same-company agent when the caller cannot read agent configuration.
  • Every response that serializes an agent redacts plaintext adapterConfig.env values as ***REDACTED*** — including GET /api/agents/me and the responses returned from create and update. Only secret_ref and user_secret_ref bindings pass through unchanged, because they are references rather than the secret value itself.

Common Fields

The agent payload is a normal JSON object. These are the fields you will see most often:

Field Meaning
name Human-friendly name. The server also derives a company-unique URL key from it.
role Role label such as ceo, engineer, or general.
title Optional display title.
appearance The agent's character (avatar) identity: { "schemaVersion": 1, "characterVersion": "cap-v1", "paletteId": "<palette>" }. See Agent Avatars for the palette ids.
avatarUrl Read-only. A ready-made URL for the agent's character image, derived from appearance.
reportsTo Parent agent in the org tree. Must be in the same company and cannot create a cycle.
adapterType Runtime type such as process, http, paperclip_runner, claude_local, codex_local, gemini_local, opencode_local, pi_local, hermes_local, cursor, or openclaw_gateway. External adapters can also be registered.
adapterConfig Adapter-specific config. Secret references are allowed inside env.
runtimeConfig Runtime settings. heartbeat.enabled defaults to false when you create an agent.
budgetMonthlyCents Monthly budget in cents. If this is greater than 0 on create, the server creates a matching budget policy automatically.
status Lifecycle state. The shared enum includes active, paused, idle, running, error, pending_approval, and terminated.
permissions Agent-level permissions: canCreateAgents and canCreateSkills (defaults to true).

List Agents

Lists agents for a company.

GET /api/companies/{companyId}/agents

Use this when you want a company-wide roster. The route excludes terminated agents by default.

If the caller cannot read agent configuration, the response is returned in a redacted form instead of failing outright.

curl -s \
  "http://localhost:3100/api/companies/{companyId}/agents" \
  -H "Authorization: Bearer <token>"
const res = await fetch(`http://localhost:3100/api/companies/${companyId}/agents`, {
  headers: { Authorization: `Bearer ${token}` },
});

const agents = await res.json();

import requests

res = requests.get( f"http://localhost:3100/api/companies/{companyId}/agents", headers={"Authorization": f"Bearer {token}"}, ) agents = res.json()


Get Agent

Returns one agent by UUID or company shortname.

GET /api/agents/{agentId}

Use this when you want the full agent record plus derived metadata:

  • chainOfCommand
  • access

The access block explains whether the agent can assign tasks and where that ability came from.

Notes:

  • If you pass a shortname instead of a UUID, the server must know the company context. Use ?companyId={companyId} when needed.
  • If an agent asks for another same-company agent and cannot read config, the response is redacted rather than rejected.
curl -s \
  "http://localhost:3100/api/agents/{agentId}?companyId={companyId}" \
  -H "Authorization: Bearer <token>"
const res = await fetch(
  `http://localhost:3100/api/agents/${agentId}?companyId=${companyId}`,
  {
    headers: { Authorization: `Bearer ${token}` },
  },
);

const agent = await res.json();

import requests

res = requests.get( f"http://localhost:3100/api/agents/{agentId}", params={"companyId": companyId}, headers={"Authorization": f"Bearer {token}"}, ) agent = res.json()


Get Current Agent

Returns the authenticated agent itself.

GET /api/agents/me

Use this from agent-authenticated flows when the agent needs to inspect its own record, chain of command, or access state.

This route is agent-only.


Compact Inbox (inbox-lite)

Returns the compact assignment list an agent needs at the start of a heartbeat.

GET /api/agents/me/inbox-lite

The response is intentionally narrower than the full issues list endpoint — each item carries just enough data to prioritise work without a second fetch:

  • id, identifier, title, status, priority
  • projectId, goalId, parentId
  • updatedAt
  • activeRun — the current run on this issue, if one is in flight
  • dependencyReady — false if any blockers are still open
  • unresolvedBlockerCount and unresolvedBlockerIssueIds

Use this route in preference to the full company issues list during heartbeat startup. Fall back to GET /api/companies/{companyId}/issues?assigneeAgentId=... only when you need the complete issue object.

This route is agent-only.

curl -s \
  "http://localhost:3100/api/agents/me/inbox-lite" \
  -H "Authorization: Bearer $PAPERCLIP_API_KEY"
const res = await fetch("http://localhost:3100/api/agents/me/inbox-lite", {
  headers: { Authorization: `Bearer ${token}` },
});
const inbox = await res.json();
import os, requests

res = requests.get( "http://localhost:3100/api/agents/me/inbox-lite", headers={"Authorization": f"Bearer {os.environ['PAPERCLIP_API_KEY']}"}, ) inbox = res.json()


Fetch Granted Secrets

Sometimes an agent needs a credential it wasn't handed at launch — a value that shouldn't live in the process environment for the whole run, or one an operator granted after the agent was already working. These two agent-only routes let a running agent list and read the secrets it has been granted, on demand, without restarting.

Both routes are run-bound. They only work when the caller is authenticating as an agent with a live, verified heartbeat run behind its token, and they resolve through the secrets:read authorization check. Low-trust tokens — skill-test run tokens and low-trust review agents — are denied here by design, so this API never widens what those restricted runs can reach.

For the full lifecycle of the secrets themselves (creating, rotating, delivery modes) see the Secrets reference. Operators decide which secrets each agent can reach from the Secret access editor in agent settings; see Secret folders for that picker.

List granted secrets

Returns just the aliases this agent is allowed to reach — never the secret values.

GET /api/agents/me/secrets

The response is { "secrets": [...] }. Each entry describes one grant:

  • key — the alias you pass to the value route below
  • name and description — the operator-facing labels
  • delivery — env, api, or both (see Delivery modes)
  • projectionClass
  • latestVersion, versionSelector, and resolvedVersion

Internal identifiers (the secret ID, binding ID, and config path) are deliberately stripped from this response.

curl -s \
  "http://localhost:3100/api/agents/me/secrets" \
  -H "Authorization: Bearer $PAPERCLIP_API_KEY"
const res = await fetch("http://localhost:3100/api/agents/me/secrets", {
  headers: { Authorization: `Bearer ${token}` },
});
const { secrets } = await res.json();
import os, requests

res = requests.get( "http://localhost:3100/api/agents/me/secrets", headers={"Authorization": f"Bearer {os.environ['PAPERCLIP_API_KEY']}"}, ) secrets = res.json()["secrets"]

Read one secret value

Returns the plaintext value for a single granted alias.

POST /api/agents/me/secrets/{key}/value

The {key} path parameter is the key you got from the list route. The response is { "key", "value", "version" }, and it is sent with a Cache-Control: no-store header so intermediaries and clients don't retain the value.

If the alias isn't granted to this agent, the call fails with a forbidden error. Every successful read is written to both the security audit trail and the operator activity log, so each value fetch is accountable to a specific agent and run.

curl -s -X POST \
  "http://localhost:3100/api/agents/me/secrets/stripe-api-key/value" \
  -H "Authorization: Bearer $PAPERCLIP_API_KEY"
const res = await fetch(
  "http://localhost:3100/api/agents/me/secrets/stripe-api-key/value",
  {
    method: "POST",
    headers: { Authorization: `Bearer ${token}` },
  },
);
const { value, version } = await res.json();
import os, requests

res = requests.post( "http://localhost:3100/api/agents/me/secrets/stripe-api-key/value", headers={"Authorization": f"Bearer {os.environ['PAPERCLIP_API_KEY']}"}, ) secret = res.json()

Both routes are agent-only.


Create Agent

Creates a new agent in a company.

POST /api/companies/{companyId}/agents

Use this when you are hiring or provisioning a new agent. In the current implementation, this route is board-only.

Important behavior:

  • The server validates the target company.
  • The new agent is created with status: "idle".
  • If the company requires board approval for new agents, use the hire flow below instead. That route creates pending_approval and records the approval payload.
  • Duplicate names are deduplicated on create. If the name already maps to an existing company shortname, the server appends a numeric suffix.
  • runtimeConfig.heartbeat.enabled defaults to false if you omit it.
  • adapterConfig.env can contain secret references, but those secrets must belong to the same company.
  • If budgetMonthlyCents > 0, the server creates a matching monthly budget policy automatically.
  • If you omit appearance, the server picks a random character palette for the new agent and saves it, so the agent keeps the same look from then on.
  • For adapters that support an instructions bundle, send the agent's instructions as instructionsBundle.files — for example {"files": {"AGENTS.md": "You are the CTO. You own technical direction."}}, with an optional entryFile. If you leave it out, the server seeds a short default AGENTS.md: a one-line role description for role: "ceo", or a one-line generic placeholder for every other role. Keep it short — Paperclip's runtime and installed skills already supply the operating procedures. Don't use adapterConfig.promptTemplate or bootstrapPromptTemplate for new agents.
  • Certain adapters apply defaults on create. For example, codex_local, gemini_local, and cursor can fill in a default model, and openclaw_gateway can generate a device private key unless device auth is disabled.

Example

curl -s -X POST \
  "http://localhost:3100/api/companies/{companyId}/agents" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Engineering Lead",
    "role": "ceo",
    "title": "CEO",
    "reportsTo": null,
    "capabilities": "Plans strategy, delegates work, reviews progress",
    "adapterType": "claude_local",
    "adapterConfig": {
      "model": "claude-sonnet-4-20250514",
      "cwd": "/Users/me/projects/company",
      "env": {
        "ANTHROPIC_API_KEY": {
          "type": "secret_ref",
          "secretId": "secret-id",
          "version": "latest"
        }
      }
    },
    "runtimeConfig": {
      "heartbeat": {
        "enabled": false
      }
    },
    "budgetMonthlyCents": 25000,
    "desiredSkills": ["strategy", "planning"],
    "metadata": {
      "team": "platform"
    }
  }'
const res = await fetch(`http://localhost:3100/api/companies/${companyId}/agents`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Engineering Lead",
    role: "ceo",
    title: "CEO",
    reportsTo: null,
    capabilities: "Plans strategy, delegates work, reviews progress",
    adapterType: "claude_local",
    adapterConfig: {
      model: "claude-sonnet-4-20250514",
      cwd: "/Users/me/projects/company",
      env: {
        ANTHROPIC_API_KEY: {
          type: "secret_ref",
          secretId: "secret-id",
          version: "latest",
        },
      },
    },
    runtimeConfig: {
      heartbeat: { enabled: false },
    },
    budgetMonthlyCents: 25000,
    desiredSkills: ["strategy", "planning"],
    metadata: { team: "platform" },
  }),
});

const agent = await res.json();

import requests

res = requests.post( f"http://localhost:3100/api/companies/{companyId}/agents", headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json", }, json={ "name": "Engineering Lead", "role": "ceo", "title": "CEO", "reportsTo": None, "capabilities": "Plans strategy, delegates work, reviews progress", "adapterType": "claude_local", "adapterConfig": { "model": "claude-sonnet-4-20250514", "cwd": "/Users/me/projects/company", "env": { "ANTHROPIC_API_KEY": { "type": "secret_ref", "secretId": "secret-id", "version": "latest", } }, }, "runtimeConfig": { "heartbeat": {"enabled": False}, }, "budgetMonthlyCents": 25000, "desiredSkills": ["strategy", "planning"], "metadata": {"team": "platform"}, }, ) agent = res.json()


Hire Agent

Creates an agent through the approval-aware hire flow.

POST /api/companies/{companyId}/agent-hires

Use this when the company may require board approval for new agents, or when you want to attach source issues to the hire request.

Important behavior:

  • The request body accepts the same core agent fields as create.
  • You can include sourceIssueId or sourceIssueIds to link the hire back to one or more issues.
  • An agent on the paperclip_runner adapter can send "inheritRuntimeFrom": "caller" to give the new hire the calling agent's runner settings and default environment. The new agent must also use adapterType: "paperclip_runner", and you can't combine this with adapterConfig, runtimeConfig, or defaultEnvironmentId — the server copies the adapter config and default environment from the calling agent. Board users can't use this option; it's rejected with 403 Forbidden.
  • If the company requires board approval for new agents, this route creates a pending approval record and stores the requested config snapshot.
  • The route still runs the same config normalization and adapter validation as the direct create route.

Update Agent

Updates an existing agent.

PATCH /api/agents/{agentId}

Use this for normal agent edits such as renaming, changing the role, adjusting the adapter config, changing the manager, or updating runtime settings.

Important behavior:

  • permissions is not accepted here. Use the permissions route below.
  • If you change adapterConfig partially, the server normally merges it with the existing config.
  • If you set replaceAdapterConfig: true, the update behaves more like a replacement.
  • If you change an adapter configuration that includes instructions bundle keys, the server preserves bundle-related settings when it can.
  • Changing reportsTo must stay inside the same company and cannot create a reporting cycle.
  • You can change the agent's character by sending a new appearance object. It must match the shape shown in Common Fields.
  • Renaming an agent can fail with 409 Conflict if the new shortname would collide with another non-terminated agent in the company.
  • Terminated agents cannot be resumed through a status patch.
  • Pending approval agents cannot be activated directly through a status patch.

Example

curl -s -X PATCH \
  "http://localhost:3100/api/agents/{agentId}" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Principal Engineer",
    "capabilities": "Designs systems, reviews architecture, and guides implementation",
    "runtimeConfig": {
      "heartbeat": {
        "enabled": true,
        "intervalSec": 300
      }
    }
  }'
const res = await fetch(`http://localhost:3100/api/agents/${agentId}`, {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    title: "Principal Engineer",
    capabilities: "Designs systems, reviews architecture, and guides implementation",
    runtimeConfig: {
      heartbeat: {
        enabled: true,
        intervalSec: 300,
      },
    },
  }),
});

const agent = await res.json();

import requests

res = requests.patch( f"http://localhost:3100/api/agents/{agentId}", headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json", }, json={ "title": "Principal Engineer", "capabilities": "Designs systems, reviews architecture, and guides implementation", "runtimeConfig": { "heartbeat": { "enabled": True, "intervalSec": 300, }, }, }, ) agent = res.json()


Update Permissions

Updates the agent-level permissions block.

PATCH /api/agents/{agentId}/permissions

This is the only supported way to change permissions. The main PATCH /api/agents/:id route rejects a permissions body.

Current permission fields:

  • canCreateAgents (required)
  • canCreateSkills (optional)
  • canAssignTasks (required) — the route applies this as a principal permission grant on the agent (tasks:assign) rather than storing it on the agent record.

Board sessions can call this route. Agent sessions can only call it when the caller is the company CEO agent.

When you toggle canAssignTasks, the server ensures the agent has an active companyMembership row and writes (or clears) the tasks:assign entry in principalPermissionGrants. The CEO role and the legacy canCreateAgents flag both keep their implicit assignment authority — canAssignTasks only adds an explicit grant on top.


Scoped Permissions and Authorization

Agents and board users share one authorization service. Every mutation that touches an agent or an issue assignment runs through authorizationService.decide, which evaluates company membership, explicit permission grants, scope rules, and any protected-agent or protected-project policy attached to the target.

Permission keys

The permissionKey field on a grant is one of:

Key What it allows
agents:create Hire or create new agents in the company, and run mutations or environment probes against agent configuration. Still satisfies agent_config:read and agent_config:update checks — but board (human) company members no longer need it to read agent configuration, skills, or config-revisions. Those reads are gated only on company membership. Agent (non-human) actors still need agents:create (or the legacy canCreateAgents flag) to read peer agents' configurations.
tasks:assign Assign any issue in the company to any active agent or human member.
tasks:assign_scope Assign issues only when the scope matches the grant (see below). Requires a structured scope on the grant.
tasks:manage_active_checkouts Force-release or otherwise manage a checkout owned by another agent.
environments:manage Manage execution workspaces and environments.
users:invite Invite new humans to the company.
users:manage_permissions Edit roles and grants for other members.
joins:approve Approve pending join requests.

A new standard agent gets canCreateAgents: true and canCreateSkills: true when it is created; low-trust agents get canCreateAgents: false. Stored agents without an explicit canCreateAgents value are treated as false. An agent with the ceo role can always create agents, whatever its flag says. New standard agents also receive fourteen direct grants when they are created or their hire is approved: agents:configure, agents:suggest-changes, skills:create, skills:suggest-changes, tools:manage_connections, tools:manage_profiles, tools:view_audit, audit:view_agent_actions, tools:use, tools:manage_runtime, inbox:manage (scoped { "responsibleUserOnly": true }), tasks:assign, tasks:assign_scope (scoped { "subtreeRootAgentId": "<the new agent's id>" }), and tasks:manage_active_checkouts. Low-trust agents and managed built-in agents don't receive them, and existing agents aren't backfilled.

Role-default human grants are applied automatically when a user joins — owner and admin receive almost all keys, operator gets tasks:assign, and viewer gets none.

Grant scope

A grant can either be company-wide (no scope) or scoped to a subset. The scope is a JSON object on the grant and the authorization service understands these keys:

  • projectId or projectIds — restrict the grant to specific projects. The shorthand project:<id> inside an allow array also works.
  • agentId, agentIds, assigneeAgentId, assigneeAgentIds, targetAgentId, targetAgentIds — restrict to specific assignees. Shorthand agent:<id> is accepted.
  • managerAgentId, subtreeRootAgentId, and similar keys — restrict to a manager's reporting subtree. Shorthand subtree:<id> is accepted.

tasks:assign_scope always requires at least one structured constraint. A grant of tasks:assign with an empty scope is treated as company-wide.

The access block on an agent

GET /api/agents/{agentId} includes an access object that summarizes the agent's assignment authority:

Field Meaning
canAssignTasks Whether the agent can currently assign work in the company.
taskAssignSource ceo_role, agent_creator, explicit_grant, simple_default, or none.
membership The agent's companyMembership row, if any.
grants All principal permission grants attached to the agent.

Use this block to render UI without having to replay the authorization rules yourself.

Protected agents, projects, and issues

Agents, projects, and issues can carry an authorizationPolicy block (inside permissions, executionWorkspacePolicy, or executionPolicy). The service understands three shapes:

  • agentVisibility.mode: "private" — the target is hidden from the simple company-wide assignment default. Assigning to it requires an explicit grant.
  • assignmentPolicy.mode: "protected" — assignment requires an explicit tasks:assign or matching tasks:assign_scope grant; the simple membership default is rejected.
  • protectedAgent.requiresApproval: true (or assignmentPolicy.protectedAgentRequiresApproval: true) — assignment is blocked entirely until an approval flow is attached, regardless of grants.

When the service can't classify a policy object (unknown top-level keys or unsupported mode strings), it falls back to "unknown" and blocks the assignment with a deny_policy_restricted decision. Treat unrecognized policy data as a hard block.

Decision reasons

When an assignment is rejected, the server returns one of these reasons in the error payload. The list is also useful when you're debugging:

Reason Meaning
allow_local_board The local implicit board is always allowed.
allow_instance_admin Instance admins bypass company-level checks.
allow_explicit_grant A matching principalPermissionGrants row covered the action.
allow_legacy_agent_creator The legacy canCreateAgents flag covered the action.
allow_self An agent acting on its own assignment or its own config.
allow_company_agent An agent acting on an issue with no agent assignee.
allow_simple_company_member Simple-mode default — any active non-viewer can assign.
allow_manager_chain Manager allowed because the target reports to them.
deny_unauthenticated No actor or no agent id on the request.
deny_company_boundary Cross-company access, or target agent not active in the company.
deny_missing_membership Principal has no active company membership.
deny_missing_grant No grant matched the requested permission key.
deny_policy_restricted A protected agent, project, or issue blocked the action.
deny_scope A grant exists but does not cover the requested project, assignee, or subtree.
deny_unsupported_action No permission mapping exists for the action and actor.

Managing member roles and grants

PATCH /api/companies/{companyId}/members/{memberId}/role-and-grants

Use this route to change a human member's role and replace their grant set in one transaction. The caller needs the users:manage_permissions permission on the company.

Request body:

Field Type Notes
membershipRole owner, admin, member, viewer (optional) When omitted, the existing role is kept. The route refuses to demote the last active owner.
status active or inactive (optional) Defaults to the current status.
grants array (optional) Replaces the member's full grant set. Each entry is { permissionKey, scope? }.

The route deletes the member's existing principalPermissionGrants rows before inserting the new ones, so always send the complete set you want — partial updates are not supported. Activity is logged as company_member.updated.


Pause, Resume, Terminate, Delete

These routes manage the agent lifecycle.

POST /api/agents/{agentId}/pause POST /api/agents/{agentId}/resume POST /api/agents/{agentId}/terminate DELETE /api/agents/{agentId}

Use them like this:

  • Pause when the agent should stop taking work temporarily.
  • Resume when the agent should become available again.
  • Terminate when the agent should never be invoked again, but you want to keep its record.
  • Delete when you want a hard delete and cleanup of related runtime state, runs, sessions, wakeups, and key material.

Important edge cases:

  • Pausing a terminated agent fails.
  • Resuming a terminated agent fails.
  • Resuming a pending approval agent fails.
  • Terminating an agent revokes all of its API keys.
  • Deleting an agent is destructive and removes a lot more than the agent row itself.

API Keys

Agents can have API keys for runtime access.

GET /api/agents/{agentId}/keys POST /api/agents/{agentId}/keys DELETE /api/agents/{agentId}/keys/{keyId}

Use keys when an agent or worker needs long-lived authentication that belongs to a specific agent.

Important edge cases:

  • A key token is shown only once at create time.
  • You cannot create keys for pending approval agents.
  • You cannot create keys for terminated agents.
  • Revoking a key marks it revoked; it is not hard-deleted.

Example

curl -s -X POST \
  "http://localhost:3100/api/agents/{agentId}/keys" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"name":"deploy-bot"}'
const res = await fetch(`http://localhost:3100/api/agents/${agentId}/keys`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "deploy-bot" }),
});

const key = await res.json();

import requests

res = requests.post( f"http://localhost:3100/api/agents/{agentId}/keys", headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json", }, json={"name": "deploy-bot"}, ) key = res.json()


Wake an Agent

Queues or coalesces a wakeup with context.

POST /api/agents/{agentId}/wakeup

Use this when you want to trigger work manually and include extra context such as an issue ID, reason, comment ID, or a payload object.

The request body supports:

  • source: timer, assignment, on_demand, or automation
  • triggerDetail: manual, ping, callback, or system
  • reason
  • payload
  • idempotencyKey
  • forceFreshSession

Important behavior:

  • The route is company-scoped.
  • Agent auth can only invoke the same agent.
  • The endpoint returns 202 Accepted.
  • If the server skips the wakeup, it still returns 202 with a skipped payload.
  • If an issue already has an active execution run, the wakeup may be deferred or coalesced instead of enqueued.
  • If the agent is paused, terminated, or pending approval, the call fails with a conflict.
  • Timer wakes can be disabled by heartbeat policy.
  • Non-timer wakes can also be disabled by policy.

Example

curl -s -X POST \
  "http://localhost:3100/api/agents/{agentId}/wakeup" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "assignment",
    "triggerDetail": "manual",
    "reason": "Issue assigned",
    "payload": {
      "issueId": "issue-123",
      "commentId": "comment-456"
    },
    "idempotencyKey": "issue-123-assignment",
    "forceFreshSession": false
  }'
const res = await fetch(`http://localhost:3100/api/agents/${agentId}/wakeup`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    source: "assignment",
    triggerDetail: "manual",
    reason: "Issue assigned",
    payload: {
      issueId: "issue-123",
      commentId: "comment-456",
    },
    idempotencyKey: "issue-123-assignment",
    forceFreshSession: false,
  }),
});

const result = await res.json();

import requests

res = requests.post( f"http://localhost:3100/api/agents/{agentId}/wakeup", headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json", }, json={ "source": "assignment", "triggerDetail": "manual", "reason": "Issue assigned", "payload": { "issueId": "issue-123", "commentId": "comment-456", }, "idempotencyKey": "issue-123-assignment", "forceFreshSession": False, }, ) result = res.json()

Common skipped responses

The exact skip reason depends on why the wakeup could not be queued. Common examples include:

  • wakeup_skipped
  • issue_execution_deferred
  • heartbeat.disabled
  • heartbeat.wakeOnDemand.disabled
  • budget.blocked

Invoke Heartbeat

Triggers a simple on-demand heartbeat.

POST /api/agents/{agentId}/heartbeat/invoke

Use this when you want a straightforward manual tick without the richer wakeup payload.

Behavior is similar to wakeup, but the response is simpler:

  • 202 Accepted
  • either a run object, or { "status": "skipped" }
  • agent auth can only invoke itself

Org Chart

Returns the company org structure.

GET /api/companies/{companyId}/org GET /api/companies/{companyId}/org.svg GET /api/companies/{companyId}/org.png

Use the JSON route when you want to build your own visualizations. Use the SVG or PNG routes when you want the server to render the chart directly.

Notes:

  • These routes omit terminated agents.
  • style is optional for the SVG and PNG variants.

Agent Avatars

Renders the character image for an agent's appearance.

GET /api/agent-avatars/{version}/{palette}/{pose}.png

You rarely need to build this URL yourself — agent responses include a ready-made avatarUrl, and live-run lists include agentAppearance and avatarUrl for each run. Reach for this route when you want a different size or pose.

The route only serves preset artwork. It doesn't look up any agent or company, so the URL tells it everything it needs:

Part Values
version cap-v1
palette One of bubblegum-sky, pink-lemonade, orchid-peach, coral-mint, lime-lagoon, arctic-blue, solar-flare, violet-ember, deep-tide, coral-current, golden-hour, tangerine-cobalt, electric-grove, flamingo-jade, cherry-pop, turquoise-cherry, ultraviolet-tide — or muted-dream for the greyed-out version.
pose One of rest, idle, listening, thinking, working, success, confused, sleepy, loading.
size (query) Pixel size: 16, 20, 24, 32, 40, 48, 64, 96, 128, 256, or 512. Defaults to 512.
scale (query) 1 or 2 (for high-density screens). Defaults to 1.
curl -s -o ada.png \
  "http://localhost:3100/api/agent-avatars/cap-v1/arctic-blue/working.png?size=128&scale=2"

Notes:

  • Images are cached for a year and carry an ETag, so repeat requests can come back as 304 Not Modified.
  • Any other query parameter, or a value outside the lists above, returns 400 Bad Request.
  • If you send too many requests at once you get 429 Too Many Requests, and if a render fails you get 503 Service Unavailable. Both carry a Retry-After header — wait and try again.

Adapter Helpers

These routes help you inspect and validate adapter environments.

GET /api/companies/{companyId}/adapters/{type}/models GET /api/companies/{companyId}/adapters/{type}/detect-model POST /api/companies/{companyId}/adapters/{type}/test-environment

Use them when you are choosing a model, auto-detecting a recommended model, or checking whether an adapter config is valid before you create or update an agent.

Important notes:

  • The adapter type must be known to the server.
  • The test-environment route needs permission to create agents in the company — the same check as the create and hire routes.
  • The server resolves secrets before running the test.
  • To test a saved agent, pass its agentId in the test-environment body. The server fills in any environment values that came back redacted from the saved config, so you don't have to re-enter them. If you're testing a switch to a different adapter, re-enter those values instead — the server won't carry hidden values across to an unrelated adapter.
  • If you leave environmentId out, the test runs in the saved agent's default environment (when you passed agentId), or else the instance's default environment. Send environmentId: null to test against the instance default explicitly.

Skills

These routes inspect and sync the skills attached to an agent.

GET /api/agents/{agentId}/skills POST /api/agents/{agentId}/skills/sync

Use them when the agent's adapter supports skill discovery and sync.

Sync request

Send the desired skill set plus a mode that tells the server how to apply it, relative to the agent's current desired skills. The mode field is required.

Field Required Notes
desiredSkills yes Array of references. Each entry is a company-skill UUID, canonical key, or unique slug. Mix and match is fine.
mode yes One of "add", "remove", or "replace". Chooses how desiredSkills merges with the current set.

mode decides the merge, keyed by skill key:

  • "add" — union: keeps the agent's other skills and adds the ones named in desiredSkills (a named skill already present is updated in place).
  • "remove" — removes only the named skills from the current set; everything else stays.
  • "replace" — destructively overwrites the complete desired set with exactly desiredSkills. This is the reconciling "send the full set" behaviour: anything missing is added and anything not in the list is removed.

If mode is missing or not one of those three values, the route returns 422 with the message: Skill sync requires mode: "add", "remove", or "replace". Use "replace" only to overwrite the complete desired skill set.

desiredSkills is also accepted at hire time on POST /api/companies/{companyId}/agents and POST /api/companies/{companyId}/agent-hires, so the agent comes online with skills already assigned. Those hire/create routes do not take mode — it applies only to /skills/sync.

curl -s -X POST \
  "http://localhost:3100/api/agents/{agentId}/skills/sync" \
  -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "desiredSkills": ["paperclip", "improve-skill"], "mode": "add" }'
await fetch(`http://localhost:3100/api/agents/${agentId}/skills/sync`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ desiredSkills: ["paperclip", "improve-skill"], mode: "add" }),
});
import os, requests
requests.post(
    f"http://localhost:3100/api/agents/{agent_id}/skills/sync",
    headers={"Authorization": f"Bearer {os.environ['PAPERCLIP_API_KEY']}",
             "Content-Type": "application/json"},
    json={"desiredSkills": ["paperclip", "improve-skill"], "mode": "add"},
)

Important notes:

  • Some adapters do not implement skill sync yet.
  • Unsupported adapters still return a useful snapshot with warnings.
  • Sync requires update permission on the target agent.
  • Skills must already be installed at the company level. The full company-skill lifecycle (file shape, import, scoping, versioning) is documented in the Skills reference.

Configuration

These routes are for reading and managing agent configuration history.

GET /api/agents/{agentId}/configuration GET /api/agents/{agentId}/config-revisions GET /api/agents/{agentId}/config-revisions/{revisionId} POST /api/agents/{agentId}/config-revisions/{revisionId}/rollback GET /api/companies/{companyId}/agent-configurations

Use them when you need to:

  • inspect the effective agent configuration
  • audit prior config changes
  • roll back to a previous revision
  • review all company agent configs in one place

Important notes:

  • If you are a board (human) member of the company, configuration and config-revision reads need only your company membership — no agents:create permission is required. Agent (non-human) actors still need agents:create (granted, or the legacy canCreateAgents flag on their own record) to read these. Mutations and environment probes always require agents:create.
  • Revisions redact secret-bearing fields before returning them, so reads never expose secrets regardless of who you are.
  • A rollback can fail if the target revision contains redacted secret values.

Instructions Bundle

These routes manage the agent's instruction files:

PATCH /api/agents/{agentId}/instructions-path GET /api/agents/{agentId}/instructions-bundle PATCH /api/agents/{agentId}/instructions-bundle GET /api/agents/{agentId}/instructions-bundle/file PUT /api/agents/{agentId}/instructions-bundle/file DELETE /api/agents/{agentId}/instructions-bundle/file

Use them when the agent's prompt instructions are stored as files instead of only inline config.

For a managed bundle, these files are the agent's persistent folder: it holds the entry file (usually AGENTS.md) plus any notes, subfolders, or binary files the agent or you add. Paperclip keeps the current files only — there's no revision history for new saves. The bundle response marks this with persistence: "agent_files". See Agents → Agent files persist across tasks for how runs read and save the folder.

Reading files

GET /api/agents/{agentId}/instructions-bundle/file?path=AGENTS.md

The path query parameter is required. The response includes the file's content and its contentHash — keep the hash, because you need it to save or delete the file.

For a managed bundle, add download=true to stream the raw bytes as an attachment instead. Use this for binary files, or files too large for the editor:

curl -s -o notes.png \
  "http://localhost:3100/api/agents/{agentId}/instructions-bundle/file?path=notes/diagram.png&download=true" \
  -H "Authorization: Bearer <token>"

Saving files

PUT /api/agents/{agentId}/instructions-bundle/file

Field Notes
path File path inside the bundle, e.g. AGENTS.md or notes/today.md.
content The full text content, up to 1 MiB.
baseHash The contentHash you read. Send null when you're creating a new file. Required for managed bundles.
clearLegacyPromptTemplate Optional. true also clears the agent's old inline prompt template. Needs permission to manage the instructions path.

Saves are guarded against lost updates: if the file changed since you read it, the save is rejected with 409 Conflict and nothing is written. Read the file again, reapply your change, and retry with the new hash. A save with no baseHash on a managed bundle returns 422.

curl -s -X PUT http://localhost:3100/api/agents/{agentId}/instructions-bundle/file \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "path": "AGENTS.md",
    "content": "You are the CTO. You own technical direction and the engineering team.",
    "baseHash": "<contentHash from your last read>"
  }'

To delete a file from a managed bundle, pass the same hash as a query parameter: DELETE /api/agents/{agentId}/instructions-bundle/file?path=notes/old.md&baseHash=<contentHash>. The entry file can't be deleted.

Storage limits

Managed bundles accept regular files and directories, including binary files. Symlinks and special files are rejected. The instruction entry remains a valid UTF-8 file of at most 1 MiB; other persisted files can use the larger per-file limit below.

A managed folder holds up to 100,000 files and folders, 256 MiB per file, and 2 GiB in total. An API save that would go over a limit returns 422 and leaves the saved files unchanged. When a run goes over, its folder changes aren't saved; the run's save receipt reports AGENT_FILES_LIMIT_EXCEEDED, and the run detail shows a storage warning. The agent keeps running either way.

Run synchronization saves only changed or deleted files. If separate runs or a browser save modify the same path, the last completed synchronization wins. Unchanged files remain intact. Current bytes live under <paperclipInstanceRoot>/companies/<companyId>/agents/<agentId>/instructions/, so include the instance filesystem in your backup as well as the database. Temporary working copies are cleaned up when their session stops and provide no new revision history.

Implementation reference: file store, managed directory path, and instruction entry validation.

Older revision and conflict routes

GET /api/agents/{agentId}/instructions-bundle/history GET /api/agents/{agentId}/instructions-bundle/revision/{revisionId} GET /api/agents/{agentId}/instructions-bundle/diff?from={revisionId}&to={revisionId} POST /api/agents/{agentId}/instructions-bundle/restore GET /api/agents/{agentId}/instructions-bundle/candidates POST /api/agents/{agentId}/instructions-bundle/candidates/{runId}/resolve

These exist for compatibility with instances that saved instruction revisions before agent files arrived. New saves never add revisions, so history only lists entries recorded before the upgrade. restore takes path, revisionId, and baseRevisionId. The candidates routes list and resolve edits preserved from older instruction-only sessions; resolve takes baseRevisionId and content.

Notes:

  • The target agent or an ancestor manager can manage the instructions path.
  • An agent can read and save its own files within what its responsible user is currently allowed to do. Reading or saving another agent's files needs permission to configure that agent.
  • A low-trust agent can't save its own instructions from ordinary work. The exception is an edit its user asks for directly in their own Agent Chat — see Instruction edits from your own chat.
  • Relative instructions paths require adapterConfig.cwd.
  • External bundles keep their existing behaviour; download=true and the baseHash rules above apply to managed bundles.

Claude Login

POST /api/agents/{agentId}/claude-login

This is board-only and only works for claude_local agents.


Claude subscription login (setup-token)

Instead of pasting an ANTHROPIC_API_KEY, a company owner can log Claude in with a Claude subscription using a setup-token flow. The flow is company-and-environment scoped (it carries no agent id, so you can run it during a hire before any agent exists), and the server derives the owner from the authenticated actor.

Route Purpose
GET /api/companies/{companyId}/claude-oauth-token-status Read whether a stored Claude OAuth token exists for the authenticated owner. Returns only the secret id and latest version — never a token. A missing or foreign value returns the same fixed 404.
POST /api/companies/{companyId}/setup-token-login-sessions Start a login session. Body: { "environmentId": "<uuid>", "adapterType": "claude_local", "overwrite"?: { … } }. Returns 201.
GET /api/companies/{companyId}/setup-token-login-sessions/active Rediscover the caller's own active session without a session id — handy after a browser reload with no local state. Returns the panel mode and the one-time prompt, sent with Cache-Control: no-store, private. A non-owner, a foreign company, or no active session all return the same fixed 404.
GET /api/companies/{companyId}/setup-token-login-sessions/{sessionId} Read the session status.
GET /api/companies/{companyId}/setup-token-login-sessions/{sessionId}/prompt Read the one-time login prompt (the authorization URL to open in a browser).
POST /api/companies/{companyId}/setup-token-login-sessions/{sessionId}/code Submit the code the browser hands back. Body: { "browserCode": "…" }.
POST /api/companies/{companyId}/setup-token-login-sessions/{sessionId}/completion Read the completion claim. Returns the non-secret storedSessionId — never a token.
POST /api/companies/{companyId}/setup-token-login-sessions/{sessionId}/cancel Cancel the session. Idempotent: a missing, terminal, or foreign session id all return 200, so the route never confirms a session exists.

Two things to know:

  • No response ever carries a token. The completion returns only the opaque storedSessionId claim; the stored value lives in the company's secrets.
  • The prompt, code, and completion calls require a confidential transport. Over a non-confidential connection the guard returns 403 — these steps move the login material, so they refuse to run in the clear.

Managed and Remote Agent Profiles

Not every agent runs from an editable adapter config. Sometimes an operator qualifies a fixed backend once — a Claude Managed Agent, or a remote runner such as AWS Bedrock AgentCore — and stores it as a company-scoped profile. Runtime inputs then copy the immutable resource identity from the profile row instead of trusting an agent's own adapter config. Credentials never live in the profile itself: managed profiles link a company secret by id, and remote profiles carry no credential at all.

Both profile APIs are board-only and company-scoped, and both POST routes upsert (keyed by profileKey) and return 201.

Managed agent profiles

GET /api/companies/{companyId}/managed-agent-profiles POST /api/companies/{companyId}/managed-agent-profiles

The request body accepts:

Field Notes
profileKey Company-unique key for the profile.
displayName Human-friendly label.
anthropicAgentId The managed agent's immutable public id.
agentVersion The qualified agent version.
environmentId The managed environment id.
defaultModel Model id. Defaults to claude-sonnet-5.
defaultMaxListCostUsd Per-list cost ceiling in US dollars. Defaults to 1 and must be positive; the server stores it internally as defaultMaxListCostCents.
apiKeySecretId Id of the company secret that holds the API key. The credential stays in the secret store.
enabled Whether the profile is active. Defaults to false.
retentionAcknowledged Whether the operator has acknowledged data retention. Defaults to false.
qualification Free-form qualification-evidence object.

The stored profile also exposes service (always anthropic_managed_agents), betaVersion, qualifiedAt, qualifiedRevision, and the usual timestamps. A profile can only be qualified/enabled once its qualification is attested. Upserts are logged as managed_agent_profile.upserted.

Remote agent profiles

GET /api/companies/{companyId}/remote-agent-profiles POST /api/companies/{companyId}/remote-agent-profiles

The list route accepts an optional ?service= filter. The request body accepts:

Field Notes
profileKey Company-unique key for the profile.
displayName Human-friendly label.
service Remote runner service. Currently only aws_bedrock_agentcore_harness is accepted.
configuration Service-specific configuration object.
enabled Whether the profile is active. Defaults to false.
retentionAcknowledged Whether the operator has acknowledged data retention. Defaults to false.
qualification Free-form qualification-evidence object.

AWS AgentCore authenticates through the runner environment's workload identity, so the profile stores no credential. Sending a credentialSecretId is rejected with 422 Unprocessable Entity, and an unsupported service value is rejected the same way. The stored profile also exposes qualifiedAt, qualifiedRevision, and timestamps. Upserts are logged as remote_agent_profile.upserted.


Common Edge Cases

  • Agent shortname lookup without company context returns 422 Unprocessable Entity.
  • A shortname collision returns 409 Conflict.
  • Updating permissions through the main update route returns 422.
  • Creating a key for a terminated or pending approval agent returns 409.
  • Pause and resume are not available for terminated agents.
  • A heartbeat-run route called with a run ID that isn't a valid UUID returns 400 Bad Request (Invalid heartbeat run ID).
  • Wakeup can be skipped even when the request is accepted, especially when policy or execution state blocks it.

Notes

This page was rewritten to match the current agent routes, auth rules, lifecycle behavior, and docs-site markdown conventions.

Changed file:

  • docs/api/agents.md