Authoring skills
Write a skill directly via memclaw_doc — the required fields, the slug and size rules, create vs update with hash-binding, and how a write is validated and staged.
A skill is a document in the skills collection. Any agent or admin can author
one with memclaw_doc op=write collection='skills'. The Skills
page shows the bare mechanics; this page is the Skill
Factory view — what the lifecycle validator requires, and what happens to
your write when the feature is enabled.
With Skill Factory enabled, a regular agent's write lands as staged
(pending Inbox review), not instantly
active. See Lifecycle & governance for the
status model and RBAC.
A minimal write
await session.call_tool("memclaw_doc", {
"op": "write",
"collection": "skills",
"doc_id": "rotate-staging-db-credentials", # = the slug
"data": {
"name": "Rotate staging DB credentials",
"slug": "rotate-staging-db-credentials",
"description": "Rotate the staging Postgres password and refresh the k8s secret.",
"summary": (
"Use when staging DB auth fails or on the 90-day rotation: "
"generate a new password, update the secret, restart the pods."
),
"domain": "ops",
"kind": "create",
"source": "agent",
"content": "## Steps\n1. ...\n2. ...\n",
},
})Required fields
The validator rejects (422) a write missing any of these top-level data
keys:
| Field | Notes |
|---|---|
name | Human-readable title. |
slug | Must equal the doc_id and match ^[a-z0-9][a-z0-9._-]{0,99}$. |
description | Short label. Capped at 160 bytes by default (skills_factory.description_max_bytes). |
domain | Free-form grouping (e.g. ops, support). |
kind | create or update (see below). |
source | One of agent, manual, forge, imported. Must be present; the server enforces a role-appropriate value (it does not default it): a regular caller must use agent; manual requires admin; forge is reserved for the Forge worker; imported is an internal back-compat/migration value. A disallowed value is rejected (403). |
content | The skill body (Markdown). Capped at 40,000 bytes. Required for every normal write; only the internal source='imported' back-compat path may omit it. |
content (the skill body, Markdown, capped at 40,000 bytes) is required for
every normal write. The sole exception is the internal source='imported'
back-compat path (skills migrated without a body) — you won't hit it on a
regular op=write.
description vs summary
Both matter, for different reasons:
descriptionis required and length-capped — a tight label.summaryis the field that gets embedded for semantic search. On theskillscollection, search ranks onsummary(falling back todescriptionif no summary is present). Write a trigger-shaped summary — "Use when …" — not a restatement of the title, or peers won't find it viamemclaw_doc op=search.
Create vs update (hash-binding)
kind distinguishes a brand-new skill from a revision of an existing one:
create— a new skill at a fresh slug.update— a revision of a live skill. The write must carrytarget.target_content_hashequal to the current skill'scontent_hash. If the live skill changed since you read it, the hashes won't match and the write is rejected — so you re-base your edit on current content rather than silently clobbering a newer version. (A skill whose target drifts is markedstale.)
An update carries the binding under data.target.target_content_hash:
await session.call_tool("memclaw_doc", {
"op": "write",
"collection": "skills",
"doc_id": "rotate-staging-db-credentials",
"data": {
"name": "Rotate staging DB credentials",
"slug": "rotate-staging-db-credentials",
"description": "...",
"domain": "ops",
"kind": "update",
"source": "agent",
"content": "## Steps\n1. ...\n",
"target": {
# the current skill's content_hash (read it first)
"target_content_hash": "sha256:…",
},
},
})If target_content_hash doesn't match the live skill, the write is rejected
with 409; if no skill exists at that slug, 404.
op=write is upsert by (collection, doc_id): re-writing the same slug with a
changed summary refreshes the embedding; same summary with different data
refreshes stored fields without re-embedding.
What the validator does to your write
On a collection='skills' write, the server (in order):
- Checks the required fields are present and typed, the slug is valid, and the
sizes are within caps — else
422. - Enforces
sourceby role (a regular caller must useagent;manualis admin-only;forgeis Forge-only — else403) and defaultsstatusby role: a regular caller's write becomesstaged; only an admin may setactive;candidateis Forge-only. - Runs a synchronous Sentinel scan. A
criticalfinding rejects the write (422); otherwise the scan state rides on the doc (clean) and feeds the promotion gate. - Stamps
content_hash,origin.agent_id, and timestamps.
So a well-formed agent write becomes a staged, scanned skill awaiting
Inbox review — not an immediately live one.
Best practices
- Lead the
summarywith the trigger ("Use when …"). It's the primary field search ranks on — description is only a fallback. - Keep
contenta runbook, not prose — numbered steps an agent can follow. - Revise with
kind='update', not a new slug, so history and hash-binding stay intact. - Expect
staged. Don't build flows that assume an agent write is instantly live; route approval through the Inbox. (auto_promote_cleanbypasses the Inbox only for fully-gated Forge candidates — not for agent writes.)
Where to look in the source
- Write validator, required fields, kinds, hash-binding:
core-api/src/core_api/services/skill_lifecycle.py - The
skillscollection /memclaw_docsurface: see Skills.
Lifecycle & governance
The skill status model, who can set each status, the six auto-promotion gates, the Sentinel security scan, and the Skills Inbox review flow.
Forge
The server-side resident that mines fleet behavior into skill candidates — how it runs, how operators trigger and observe it, and the knobs that tune it.