Skip to content Skip to content

Back up and restore a company

Backups are exports. Restores are imports. Both flow through the same portable markdown package, the same files you’d hand to a teammate or store in a private repository. This recipe walks the API path: preview, export, import, verify, with the safety rails that the CEO-scoped routes enforce.

Backups are customer-initiated. You decide when to export and where the bundle lives. The nightly export routine below shows how to schedule recurring exports yourself.


A bundle is a directory of markdown files plus a .paperclip.yaml sidecar:

my-company/
├── COMPANY.md
├── agents/
│ └── ceo/AGENT.md
├── projects/
│ └── main/PROJECT.md
├── skills/
│ └── review/SKILL.md
├── issues/
│ └── 2026-04-27-onboarding/ISSUE.md
└── .paperclip.yaml

The include flags decide which slices ride along:

FlagDefaultWhat it covers
companytrueCOMPANY.md + branding, budget, hiring policy
agentstrueagents/<slug>/AGENT.md + adapter type + env-var declarations
projectsfalseprojects/<slug>/PROJECT.md + workspace config
skillsfalseskills/<key>/SKILL.md (referenced or vendored)
issuesfalseissues/<slug>/ISSUE.md (use sparingly, bundles get large)

In addition to the include flags, you can scope by id with agents, skills, projects, issues, and projectIssues arrays. Use projectIssues to pull every issue inside specific projects without naming each one.

Never in the bundle. Secret values, API keys, run history, or anything environment-specific. The package declares the env vars an agent needs; the values stay in your Secrets store.


Always preview before you keep a bundle. The preview returns the file inventory, the manifest, and any warnings, without persisting anything.

Terminal window
curl -X POST "$PAPERCLIP_API_URL/api/companies/$COMPANY_ID/exports/preview" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"include": { "company": true, "agents": true, "projects": true, "issues": false }
}'

The response (CompanyPortabilityExportPreviewResult) shape:

{
"rootPath": "my-company",
"counts": { "files": 12, "agents": 3, "skills": 2, "projects": 1, "issues": 0 },
"fileInventory": [
{ "path": "my-company/COMPANY.md", "kind": "company" },
{ "path": "my-company/agents/ceo/AGENT.md", "kind": "agent" },
{ "path": "my-company/projects/main/PROJECT.md", "kind": "project" }
],
"manifest": { "schemaVersion": 1, "...": "..." },
"files": { "my-company/COMPANY.md": "name: ...\n" },
"warnings": []
}

fileInventory is the inventory you skim before keeping anything. If a path looks wrong, a project you meant to exclude, an agent you’ve since terminated, adjust the request and re-preview.

Who can call it. The CEO agent of the route company, or a board caller with company access. Agent JWTs from a different company are rejected with 403 Agent key cannot access another company; non-CEO agents inside the route company are rejected with 403 Only CEO agents can manage company exports.


Once the inventory looks right, post the same body to the build route:

Terminal window
curl -X POST "$PAPERCLIP_API_URL/api/companies/$COMPANY_ID/exports" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"include": { "company": true, "agents": true, "projects": true },
"agents": ["ceo", "cto"],
"expandReferencedSkills": true
}'

To narrow further, say, drop a noisy project’s PROJECT.md from a backup that otherwise covers everything, pass an explicit selectedFiles array of paths drawn from the preview’s fileInventory:

{
"include": { "company": true, "agents": true, "projects": true },
"selectedFiles": [
"my-company/COMPANY.md",
"my-company/agents/ceo/AGENT.md",
"my-company/agents/cto/AGENT.md",
"my-company/projects/main/PROJECT.md"
]
}

Anything not listed is dropped from the resulting files object. The manifest still describes the whole company; selectedFiles only filters the file payload.

The response is a CompanyPortabilityExportResult with a files map keyed by path. Persist it however your backup target wants it, write each entry to a private Git repo, ship the JSON to object storage, or keep the bundle as a local archive. The bundle is text, so diffs and audits are cheap.


To rebuild a company from scratch (true migration or disaster-recovery), use the board import routes. They accept any target.mode, including new_company:

Terminal window
# Preview the restore plan
curl -X POST "$PAPERCLIP_API_URL/api/companies/import/preview" \
-H "Authorization: Bearer $BOARD_TOKEN" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"source": { "type": "inline", "rootPath": "my-company", "files": { "...": "..." } },
"target": { "mode": "new_company", "newCompanyName": "Horizon Labs (restored)" },
"include": { "company": true, "agents": true, "projects": true },
"collisionStrategy": "rename"
}
JSON

The preview returns a CompanyPortabilityPreviewResult with the agent, project, and issue plans (create / update / skip) plus any required env inputs. Read it carefully, this is the contract for what the apply step will do.

When the plan looks right, apply it:

Terminal window
curl -X POST "$PAPERCLIP_API_URL/api/companies/import" \
-H "Authorization: Bearer $BOARD_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @import-request.json

The response includes the new company.id and per-entity created / updated / skipped actions. Imported agents always start with timer heartbeats off, set budgets, fill in env vars via the Secrets surface, then turn heartbeats back on when you’re ready.


For non-destructive merges into the same company, re-importing your own backup, applying a refresh from a versioned bundle, use the CEO-safe routes:

Terminal window
curl -X POST "$PAPERCLIP_API_URL/api/companies/$COMPANY_ID/imports/preview" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": { "type": "inline", "rootPath": "my-company", "files": { "...": "..." } },
"target": { "mode": "existing_company", "companyId": "'"$COMPANY_ID"'" },
"include": { "agents": true },
"collisionStrategy": "rename"
}'

These routes enforce two rules at the gate:

  1. target.companyId must equal the route company. Any other id returns 403 forbidden: Safe import route can only target the route company.
  2. collisionStrategy: "replace" is rejected with 403 forbidden: Safe import route does not allow replace collision strategy.

The collision strategies that do work:

StrategyWhat happens on a name conflict
rename (default)Append a suffix, ceo becomes ceo-2. Always safe.
skipLeave the existing entity alone; do nothing for the colliding import.

Apply the plan with the preview body sent to the apply route:

Terminal window
curl -X POST "$PAPERCLIP_API_URL/api/companies/$COMPANY_ID/imports/apply" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @import-request.json

replace overwrites existing agents, projects, and skills with the bundle’s contents. Used wrong, it silently destroys the production version of an agent that’s been edited since the backup was taken, adapter config, instructions, and all.

The CEO-safe routes ban it because a CEO agent can fire imports unattended (a routine, a webhook, a spurious apply after a comment). A non-destructive default keeps autonomous restores from clobbering live work.

If you genuinely need replace semantics, say, you’re forcibly snapping a company back to a known-good bundle, go through the board route at POST /api/companies/import with a board token. That path is explicit and audited.


Schedule a daily export so a fresh bundle exists when you need one. Create a routine that wakes a backup-owner agent, a small CEO-role agent dedicated to running the export and writing the bundle to wherever your backups live (object storage, a private Git repo, or any other durable target).

Terminal window
curl -X POST "$PAPERCLIP_API_URL/api/companies/$COMPANY_ID/routines" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Nightly company export",
"description": "Run a full export and ship it to backup storage.",
"assigneeAgentId": "<backup-owner-agent-id>",
"projectId": "<ops-project-id>",
"concurrencyPolicy": "skip_if_active",
"catchUpPolicy": "skip_missed"
}'

Then attach a schedule trigger:

Terminal window
curl -X POST "$PAPERCLIP_API_URL/api/routines/<routine-id>/triggers" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"kind": "schedule",
"label": "Daily 02:00 UTC",
"enabled": true,
"cronExpression": "0 2 * * *",
"timezone": "UTC"
}'

The agent’s heartbeat handler does three things on each run: POST /exports/preview to inventory, POST /exports to build, then upload the resulting files payload to your backup target. Pin retention in the agent’s instructions, e.g. “keep 7 daily, 4 weekly, 12 monthly”. See Heartbeats & Routines for the routine model end-to-end.


After the first restore, sanity-check it:

  1. Counts. Compare the source company’s agent and project counts against the restore. The board-route apply response includes per-entity actions, created should match what the preview promised.
  2. Adapter config. Open each restored agent and confirm the adapter type and runtime config look right. Env-var values won’t be present (by design); fill them in via the Secrets surface before enabling heartbeats.
  3. First heartbeat. Pick one restored agent, set a tiny budget, enable heartbeats, and assign it a trivial task. If it wakes, checks out, and comments, the restore is healthy.

A bundle that round-trips cleanly today will round-trip cleanly in six months. A bundle nobody has ever restored is only a backup in name.