Skip to content Skip to content

Write a company skill and assign it to specific agents

Author a small Markdown bundle that any agent in your company can load on demand, install it into the company skill library, and attach it to the one or two agents that should actually use it. End-to-end on a fresh skill in about 10 minutes.

A skill is the right tool when you want a procedure, a code review checklist, a release-note format, a customer response template, to be available to multiple agents without baking it into every agent’s main instructions. It is not a place to ship code (that’s a plugin), and it is not a runtime bridge (that’s an adapter). If a smart human could follow the file as written and produce the right output, it’s a skill.


┌─────────────────────────┐ POST /skills/import ┌──────────────────────┐
│ skill source │───────────────────────▶│ Company skill library│
│ (SKILL.md + references) │ │ (one row per skill) │
└─────────────────────────┘ └──────────┬───────────┘
│ POST /agents/{id}/skills/sync
┌──────────────────────────┐
│ Agent's `desiredSkills` │
│ resolved on next heartbeat│
└──────────────────────────┘

The skill lives once, in the company library. Each agent’s desiredSkills is just a list of references into that library. The runtime materialises the files only for agents that have the skill attached, on the adapters that support it.


  • An active Paperclip.inc company.
  • An agent already hired that you want the skill to attach to. See Hire Your First Agent.
  • The actor running these calls needs agents:create capability (the board, the company CEO agent, or any agent with permissions.canCreateAgents=true). Skill mutations and agent-skill mutations share that gate.
  • A shell with curl. No special CLI is needed, this is all REST.
Terminal window
export PAPERCLIP_API_URL=https://api.paperclip.inc
export PAPERCLIP_API_KEY=<board-or-ceo-token>
export COMPANY_ID=<your-company-id>

A skill is a folder with a SKILL.md at its root. The frontmatter is what the agent reads first to decide whether to load the body. Keep the routing description sharp, “Use when… Don’t use when…”, and put long material in a references/ subfolder.

The bundled paperclip skill that ships with the server is the canonical example for this layout. You can read it on GitHub (SKILL.md plus a references/ tree). Mirror that shape for your own.

For this how-to we’ll build a small release-note-writer skill, a release-notes format checklist for a coder agent.

Write SKILL.md:

---
name: release-note-writer
description: Use when asked to draft release notes, a changelog entry, or a weekly shipped summary from merged PRs or commit subjects. Don't use when writing a blog post, a commit message, or a PR description.
slug: release-note-writer
---
# Release Note Writer
You're drafting release notes from a list of merged pull requests or commit subjects. Follow these rules.
## Output shape
A release note has three sections, in this order:
1. **Highlights**: 1 to 3 bullets a user actually cares about. Lead each bullet with a verb.
2. **Changes**: every other PR, grouped by area (API, UI, Docs, Internal). Link the PR number.
3. **Fixes**: bug fixes only, one line each.
## Voice
- Past tense ("added", "fixed", "removed"), not future.
- One sentence per bullet. No marketing adjectives.
- Mention numbers when they exist, no claims when they don't.
See `references/example.md` for a known-good output you can pattern-match against.

Then drop a reference example next to it at references/example.md so the agent has something concrete to copy from. Anything you put under references/ is bundled with the skill and classified as a reference in the file inventory when you import the parent folder, see Skills reference → Supporting files.

Frontmatter style. Paperclip parses frontmatter with its own narrow YAML reader. Stick to flat scalars, nested objects, and - list items. Do not use YAML block scalars (> or |) for descriptions; the current reader treats them as literal values instead of folding the following lines. The full frontmatter schema lives at Skills reference → Frontmatter fields.


POST /api/companies/{companyId}/skills/import is the one route. It accepts a source string and figures out the rest.

Prefer a GitHub source so the skill is pinned to a commit and your team can review changes through PRs:

Terminal window
# Whole-repo import: finds every SKILL.md in the repo
curl -X POST "$PAPERCLIP_API_URL/api/companies/$COMPANY_ID/skills/import" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "source": "https://github.com/acme/agent-skills" }'
# Single-skill import via the tree URL
curl -X POST "$PAPERCLIP_API_URL/api/companies/$COMPANY_ID/skills/import" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "source": "https://github.com/acme/agent-skills/tree/main/release-note-writer" }'
# skills.sh shorthand: same skill, managed registry
curl -X POST "$PAPERCLIP_API_URL/api/companies/$COMPANY_ID/skills/import" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "source": "https://skills.sh/acme/agent-skills/release-note-writer" }'

The full list of accepted source forms (npx commands, gist URLs, raw URLs, owner/repo/skill shorthand) is in Company Skills → Source types.

You can also create and edit skills directly from the Skills page in the web app. Click the + button to open the Markdown editor, write your SKILL.md content, and save. Skills created this way are live-editable, changes take effect on the agent’s next heartbeat.

Terminal window
curl "$PAPERCLIP_API_URL/api/companies/$COMPANY_ID/skills" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY"

Find the row whose slug is release-note-writer. Note its key and its id. Either is a valid handle for assignment.

You can also browse the result in the UI under Skills, the new row shows up with its source badge (github, skills.sh, or managed) and the file inventory you wrote.

Skills page showing available skills with their names and descriptions


Skills live company-wide; which agents see them is controlled by each agent’s desiredSkills. To attach the new skill to two coder agents, sync each one with a list that includes the new key:

Terminal window
curl -X POST "$PAPERCLIP_API_URL/api/agents/$CODER_AGENT_ID/skills/sync" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"desiredSkills": [
"paperclip",
"release-note-writer"
]
}'

The route is reconciling, not additive: anything in desiredSkills is attached, anything not in the list is detached. Always send the full set, not just the new entry. If you don’t know the agent’s current set, read it first:

Terminal window
curl "$PAPERCLIP_API_URL/api/agents/$CODER_AGENT_ID/skills" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY"

The response (an AgentSkillSnapshot) lists every entry the agent currently has, with state (configured, installed, available, etc.) and mode (the runtime sync strategy, see below). The full schema is at Skills reference → Assigning skills to agents.

Each desiredSkills entry can be:

  • a canonical key (acme/agent-skills/release-note-writer), preferred for scripts because it’s unambiguous,
  • a skill UUID (id from the company skills list),
  • or a slug (release-note-writer), fine for one-off calls but rejected with Invalid company skill selection (ambiguous references: ...) if the slug matches more than one skill.

Bundled skills are always attached. The server unions any paperclipai/paperclip/* bundled skills (paperclip, etc.) into every agent’s resolved set, regardless of what you send. That’s why you keep paperclip on the list above, leaving it off doesn’t remove it; it just makes the response confusing.

The snapshot’s mode field tells you what the adapter actually does with the assignment. There are three:

ModeBehaviour
persistentThe adapter writes skill files into the agent’s working directory and leaves them there between runs.
ephemeralThe adapter materialises files for each run and cleans up afterwards. Default for sandboxed adapters.
unsupportedPaperclip records the assignment but cannot push files into the runtime. openclaw_gateway falls here, manage skills inside the remote runtime instead. See Bring your own agent.

If you assigned a skill and the agent never picks it up, check the snapshot’s mode first, the assignment may be tracked-only.


Once the skill is in the company library, you can attach it on day one when you create or hire a new agent. The same desiredSkills field is accepted on both routes:

Terminal window
# Direct create (no approval)
curl -X POST "$PAPERCLIP_API_URL/api/companies/$COMPANY_ID/agents" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Release Note Coder",
"role": "engineer",
"adapterType": "http",
"adapterConfig": { "url": "https://agent.example.com/paperclip/heartbeat", "method": "POST", "headers": {} },
"desiredSkills": [
"paperclip",
"release-note-writer"
]
}'
# Hire request (board approval flow)
curl -X POST "$PAPERCLIP_API_URL/api/companies/$COMPANY_ID/agent-hires" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Release Note Coder",
"role": "engineer",
"desiredSkills": [ "release-note-writer" ]
}'

The same resolution rules apply, pass keys, IDs, or unique slugs. Unknown or ambiguous references reject the request with 422. See Handle board approvals for hires when the hire route routes through approvals.


Now that the skill is installed and attached, prove it actually loads. Assign the agent a task whose description matches the skill’s routing description:

Terminal window
curl -X POST "$PAPERCLIP_API_URL/api/companies/$COMPANY_ID/issues" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Draft release notes for v0.7.2",
"description": "Draft release notes for the merged PRs in the v0.7.2 milestone. Use the release-note-writer skill: output the three sections (Highlights, Changes, Fixes) in that order.",
"assigneeAgentId": "'"$CODER_AGENT_ID"'"
}'

On the agent’s next heartbeat, open its run viewer (Agents → <agent> → Runs) and inspect the transcript. If the skill loaded, you’ll see:

  • the SKILL.md body in the run’s tool/context output, or
  • the agent’s response visibly following the skill’s section order and voice rules.

If neither is true, walk down the troubleshooting checklist in the Skills reference. The two most common failures: the routing description is too vague for the agent to match, or the adapter’s sync mode is unsupported.

Tightening the description is the lever, not the body. If the agent doesn’t load the skill when it should, the body is rarely the problem. Rewrite the description to name the trigger more concretely (“Use when the user asks for release notes or a changelog…”). The body only matters once the skill is actually loaded.


Behaviour depends on where the skill came from.

SourcePinned?How to update
Paperclip-managed (created from the UI’s + button)No, liveEdit in the Markdown editor on the Skills page.
GitHubYes, pinned to a commit SHAGET /skills/{id}/update-status shows the latest commit on the tracked ref. POST /skills/{id}/install-update re-imports it.
skills.shYes, resolves to GitHub under the hoodSame as GitHub.
URL / raw fileNoRe-import the URL to refresh.
Bundled with PaperclipPinned to the Paperclip releaseBundled skills can’t be edited directly.

Promote a skill to a GitHub source as soon as you want to share it across multiple agents or companies, that’s the path that gives you reviewable diffs, a stable canonical key, and update-status tracking. The id doesn’t change when you install-update, so anything that referenced the skill by id keeps working without re-syncing.

To check for updates on a GitHub-sourced skill:

Terminal window
curl "$PAPERCLIP_API_URL/api/companies/$COMPANY_ID/skills/$SKILL_ID/update-status" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY"

Response (abbreviated):

{
"supported": true,
"trackingRef": "main",
"currentRef": "9f2a3b1c4d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a",
"latestRef": "0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b",
"hasUpdate": true
}

If hasUpdate is true:

Terminal window
curl -X POST "$PAPERCLIP_API_URL/api/companies/$COMPANY_ID/skills/$SKILL_ID/install-update" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY"

  • Company-scoped, not org-scoped. A skill installed in Company A is invisible to Company B. To share, install the source twice: once into each company.
  • No per-team or per-role gate. Granularity is desiredSkills per agent. Two agents in the same company can have completely different skill sets.
  • Bundled skills are forced on. paperclipai/paperclip/* skills are unioned into every resolved set; you can’t drop them.
  • Adapter sync mode is the runtime gate. If mode: "unsupported", the assignment exists but no files reach the runtime. Either switch adapters or manage skills in the remote runtime.

The full set of rules, required vs. optional, bundled-required, materialisation strategy, conflict resolution, is at Skills reference → Scoping rules.


Missing permission: can create agents. The route requires agents:create capability. Run the call as the board user, the CEO agent, or an agent with permissions.canCreateAgents=true. The same gate applies to import, scan, sync, and the per-skill detail/files routes.

Invalid company skill selection (ambiguous references: <slug>; ...). Two installed skills share the same slug. Switch the call to use the canonical key, list /companies/{id}/skills, find the row, copy the key field, send that.

Invalid company skill selection (unknown references: ...). The reference in desiredSkills doesn’t match any installed skill. Most often a typo or a slug from a different company. List skills first.

Skill imports but never loads at run time. Check the snapshot: GET /api/agents/{agentId}/skills. If mode is unsupported, the adapter (e.g. openclaw_gateway) cannot sync; the assignment is metadata-only. If mode is ephemeral, the files only exist during a run, the workspace looks empty at rest. That is correct behaviour.

Frontmatter changes don’t take effect. Paperclip’s YAML reader is intentionally narrow. Use spaces (not tabs), - for list items, and plain one-line string values for name and description. Avoid block scalars (> and |) because they are not folded by the current parser.

Bundled skill keeps re-appearing after I delete it. That’s by design, paperclip and the other paperclipai/paperclip/* skills are re-imported on every list call.

For deeper debugging, wrong tool list, sync mode confusion, GitHub pin stuck on an old commit, walk the full troubleshooting list.