Skip to content

Agent Skills

Memoturn implements the agentskills.io v1 spec for per-project skill bundles. A “skill” is a SKILL.md file (frontmatter + body) plus optional supporting files (scripts/, references/, assets/, anything). Agents discover skills via name + description, load the body when relevant, then read individual bundled files on demand.

The progressive-disclosure model: cheap to list, only pay the body cost when a skill matches.

deploy-runbook/
├── SKILL.md # frontmatter + markdown body — required
├── scripts/
│ └── pre-flight.sh
├── references/
│ └── rollback.md
└── assets/
└── topology.png

SKILL.md opens with a YAML frontmatter block:

---
name: deploy-runbook
description: |
Coordinated procedure for shipping a worker change to staging, soaking,
and rolling forward to production. Use when the change touches a Cloudflare
binding or migrates the schema.
license: MIT
compatibility: cloudflare-workers >= 3
metadata:
team: platform
oncall-channel: "#deploys"
when_to_use: |
Any code change that ships to api.memoturn.ai.
allowed-tools:
- bash
- filesystem
---
# Deploy Runbook
1. Run `scripts/pre-flight.sh`
2. ... full body in markdown

Core spec fields (name, description, license, compatibility, metadata, allowed-tools) are validated strictly. Tool-specific extension fields (Anthropic’s when_to_use, paths, hooks, disable-model-invocation, etc.) round-trip verbatim — store anything your agent runtime understands.

Terminal window
# install or update from a directory
memoturn skill install ./skills/deploy-runbook
# install a single SKILL.md (no bundled files)
memoturn skill install ./skills/quick-tip/SKILL.md

The CLI walks the directory, reads SKILL.md + every non-hidden file under it (paths preserved relative to the skill root), validates the frontmatter, and uploads. Files larger than 1MB are rejected.

Re-installing the same name updates in place — Postgres is upsert + R2 is overwrite. Soft-deleted skills (via forget_skill) don’t block reuse of their name; the partial unique index only covers active rows.

Two surfaces: enumerate everything cheaply, or semantic search by description.

const { skills } = await mt.listSkills();
for (const s of skills) {
console.log(`${s.name}\t${s.description}`);
}

Returns the discovery layer — name + description + a few manifest surface fields per active skill. Cheap; this is what agents call up-front before deciding what to load.

Once you’ve picked a skill, load the body:

const skill = await mt.getSkill("deploy-runbook");
if (skill) {
console.log(skill.manifest.description);
console.log(skill.body); // full markdown body, no frontmatter
}

Returns null if the skill doesn’t exist (or has been forgotten) so callers can branch on presence without try/catch.

Skills can ship arbitrary supporting files. Agents read them only when actually needed — keeps the activation cost low even for large bundles:

const file = await mt.getSkillFile("deploy-runbook", "scripts/pre-flight.sh");
if (file) {
console.log(`${file.size} bytes:\n${file.content}`);
}

Path traversal is server-side rejected — leading /, \, .., and reserved SKILL.md are all 400’d. Paths are POSIX-style relative to the skill root regardless of the originating OS.

Soft-delete by name:

Terminal window
memoturn skill forget deploy-runbook

The Postgres row gets forgotten_at set and the Vectorize entry is dropped, so list_skills and mode=skills searches no longer surface it. Bundled R2 files are intentionally retained — recovery is one schema-level update if you change your mind. A subsequent install_skill with the same name creates a fresh active row alongside the tombstone.

layerwhat’s there
Postgres skillsmanifest jsonb (full validated frontmatter), description, lifecycle timestamps, soft-delete marker, embedding pointer
R2 ${slug}/${name}/SKILL.mdthe SKILL.md text verbatim, frontmatter included
R2 ${slug}/${name}/<path>every bundled file at its original relative path
Vectorize kind=skilldescription embedding for the dense leg of mode=skills

Writes are serialized through the per-project Durable Object so install/forget against the same name don’t race. The partial unique index on (project_id, name) WHERE forgotten_at IS NULL is the second line of defense.