Skills
A skill is a markdown file that gives an agent one scoped capability. What it is, how it differs from a tool and an agent, and where it lives.
Reading this as an agent? The same page in raw markdown: /docs/skills.md
A skill is a markdown file with YAML frontmatter. It holds instructions for one job, it is stored per workspace, and it is run by name. This page explains what a skill is and where it sits between a tool and an agent. Everything below was read out of the shipped code on 2026-09-06; the limits section names what is declared but not wired.
Skill, tool, agent
These are the three rungs of the autonomy ladder, and each compiles to a different receiver.
| Rung | Receiver | What it is |
|---|---|---|
| tool | tools:composio, or a named receiver | an integration; no judgment |
| skill | skill:run | a capability, scoped by its own file |
| agent | agent:run | an actor with open judgment |
The clearest mechanical difference is in the workflow run guard. In
one.ie/web/src/lib/step-ceiling.ts, stepAllowed() checks a tool step’s
receiver name against the run’s enabledToolIds list and an agent step’s
actorId against enabledAgentIds. skill:run is exempt from both, because a
skill name is a different namespace from a receiver name. It is exempt from the
rung ceiling too: stepWithinCeiling() reads the receiver’s explicit
roleAction, and skill:run declares none, so the comparison passes. Inside a
workflow run, the only thing standing between a step and a skill is authority
over the workspace holding it.
A workflow step picks its rung from the shape of its config, in
workflow-executor.ts’s stepBinding(): { receiver } dispatches verbatim,
{ composio } routes to tools:composio, { actorId } becomes agent:run,
and { skill } becomes skill:run. Two more shapes sit between them —
{ entity } compiles to the record receiver for its dimension, { lifecycle }
to entity:tag. A step naming none of the six is a no-op that traverses
without side effects.
The file
A skill opens with --- and a YAML block. The parser
(one.ie/web/src/lib/skill/parser.ts, mirrored in
packages/cli/src/skill-parser.ts) is deliberately lenient: it reads scalars and
- block lists, retries once with unquoted colons stripped, and emits
diagnostics rather than throwing.
Fields the loader lifts into a Skill object: name, title, summary,
description, price, accepts, trigger, applyTo, tags, when_to_use,
inputSchema, outputSchema, version, license, compatibility, scripts,
references, assets, evals.
Only a few fields change behaviour today. inputs and ssr are read by
separate parsers off the raw frontmatter, not through the loader:
| Field | Read by |
|---|---|
name | addressing — the string skill:run resolves |
description | skills:list when queried by name, to label the skill |
inputs | skills:list when queried by name; name|Label|required lines |
evals | skill eval and skill_eval, against POST /api/eval |
ssr | skill:run, which projects the answer over reference_sets |
price | stamped into frontmatter on import |
The rest is carried metadata. Missing pieces get filled in rather than refused:
no name and the path stem becomes the name, no description and the first
body paragraph becomes one, a triggers field folds into the description. Each
substitution raises a diagnostic — no-frontmatter, missing-name,
name-mismatch, name-too-long, description-from-body, triggers-merged,
lenient-yaml — which oneie skill validate prints.
The repo’s shared pool shows the shape at scale: 152 skills at the top level of
one.ie/ai/skills/, each a directory holding a SKILL.md, and 324 SKILL.md
files counting the nested sub-pools (platforms/, strategies/, content/,
seo/ and four others). 51 of those directories carry an evals/evals.json
bank beside the SKILL.md.
Where a skill lives
A workspace’s catalog is an R2 prefix, {slug}/skills/. Three key shapes live
there, and which one a skill has decides which readers see it.
{slug}/skills/{name}.md flat — skill:save writes it
{slug}/skills/{name}/SKILL.md dir — skill import writes it
{slug}/skills/{name}/ref.json ref — sealed import, no body
getSkillOrRef() reads all three, in that order of preference: ref, then
directory, then flat. getSkill() reads only the flat key. skill:run uses the
first; agent:run uses the second. That asymmetry is named again below.
Running one
signal("skill:run", { skill: "copywriting", topic: "launch" }). Everything
past skill is the skill’s input. The receiver resolves the workspace, loads
the body from R2, and posts one bounded turn to the channels runtime with that
body as the instructions. It returns { ok, text, skill }. Web never calls a
model itself for this — a missing or unreachable channels runtime comes back as
channels_not_configured, channels_<status> or channels_unreachable, a
clean failure rather than a thrown run.
Authority is checked against the workspace whose catalog is read, not the
caller’s own. runSkill takes the caller’s workspace as-is; a slug naming a
different one goes through decide() for manage_workflows and comes back
forbidden or unavailable. See Authority and access.
One structured field can come back beside the text. If the answer carries the
eight-field spec with a real JSON array under Deliverables:, skill:run adds
deliverables so a workflow step can fan out over it. Prose bullets add
nothing, so a bad answer fails loudly instead of writing zero rows.
Sealed skills
A sealed skill is a ref.json in the importer’s catalog naming the publisher
and the skill. The body never leaves the publisher’s prefix. On a run,
skill:run follows the ref, loads the publisher’s body, optionally checks a D1
entitlement, and returns { ok, text, sealed: true }. The importer can invoke
it and cannot read it.
Sealing is decided server-side, not by the caller: POST /api/skill/import
looks up an active market_listings row for (publisher, name) and seals only
when its visibility is sealed. The request must carry publisher for that
lookup to happen at all.
Surfaces
| Where | Names |
|---|---|
| Receivers | skill:run, skill:save, skills:list |
| CLI | oneie skill — 11 subcommands |
| MCP | skill_run, skills_list, list_skills, discover_skill, skill_eval, unimport_skill |
| HTTP | /api/skill/import, /api/skills/workspace, /api/skills/:name/fork, /api/skills/:name/enable, /api/eval |
skill:run and skills:list carry surfaces: { mcp: true } in the receiver
registry, so their MCP tools are derived from the contract rather than
hand-written. The other four MCP tools are curated modules. See
MCP server and CLI.
What does not work yet
- An agent’s
skills:frontmatter is not read at runtime.resolvePersonaparses it and carries it onto the persona, andchannels/src/personas.tssays so in the field’s own comment: nothing in channels readspersona.skills. The same holds for theskillResolvermap form. The wired path isagent:runwith an explicitskillargument, which folds the body into the instructions for that one turn. agent:runloads only flat-key skills. It callsgetSkill(), which reads{slug}/skills/{name}.mdand nothing else. A skill that arrived throughskill importlives at{name}/SKILL.md, so the load returns null and the agent runs with no skill body and no error.skills:listqueried by name is flat-key too. The unfiltered listing sees every key shape, but thenamebranch reads throughgetSkill(), so asking for an imported skill by name answers{ skills: [] }— nodescription, noinputs.oneie skill forkdoes not copy anything. It fetches the workspace list, refuses if the skill is sealed, and prints a success line. The copy isPOST /api/skills/:name/fork, which the CLI never calls.- That fork route is flat-key too. It reads through
getSkill(), so forking an imported skill returns 404. oneie skill refreshis a stub. It always answersrefreshed: 0and ignores the refs passed to it.oneie skill publishalways fails, withagentskills.io API key required — set AGENTSKILLS_API_KEY.price:does not bill a run. Import stamps it into the frontmatter and the import response echoes it.skill:runcharges nothing, andskills:listdoes not return it; the only paywall on the run path is the D1 entitlement check on a sealed ref.agent_skillsis display state.POST /api/skills/:name/enablewrites rows into that D1 table, and the only readers are the/u/[slug]/skillspage, which uses them to show which agents a skill is wired to, and a fleet probe that counts them. No runtime consults the table when running a skill.