MemClaw / docs
Skill Factory

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:

FieldNotes
nameHuman-readable title.
slugMust equal the doc_id and match ^[a-z0-9][a-z0-9._-]{0,99}$.
descriptionShort label. Capped at 160 bytes by default (skills_factory.description_max_bytes).
domainFree-form grouping (e.g. ops, support).
kindcreate or update (see below).
sourceOne 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).
contentThe 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:

  • description is required and length-capped — a tight label.
  • summary is the field that gets embedded for semantic search. On the skills collection, search ranks on summary (falling back to description if no summary is present). Write a trigger-shaped summary — "Use when …" — not a restatement of the title, or peers won't find it via memclaw_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 carry target.target_content_hash equal to the current skill's content_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 marked stale.)

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):

  1. Checks the required fields are present and typed, the slug is valid, and the sizes are within caps — else 422.
  2. Enforces source by role (a regular caller must use agent; manual is admin-only; forge is Forge-only — else 403) and defaults status by role: a regular caller's write becomes staged; only an admin may set active; candidate is Forge-only.
  3. Runs a synchronous Sentinel scan. A critical finding rejects the write (422); otherwise the scan state rides on the doc (clean) and feeds the promotion gate.
  4. 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 summary with the trigger ("Use when …"). It's the primary field search ranks on — description is only a fallback.
  • Keep content a 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_clean bypasses the Inbox only for fully-gated Forge candidates — not for agent writes.)

Where to look in the source