Custom Skills¶
Skills are reusable instructions the AI auto-invokes when a user's request matches their description. They're the cheapest, fastest way to teach Doable's AI a new capability: no code deploy, no PR, just a markdown file and a save button.
This page is the full reference: data model, scoping, manifest format, file companions, invocation lifecycle, and a worked example end-to-end.
What is a skill?¶
A skill is a folder on disk that the AI's host (the Copilot SDK) discovers as part of its skillDirectories list. Each folder contains:
SKILL.md: the skill body, with YAML frontmatter that gives the skill anameand adescription. The AI reads this on every turn and decides whether to invoke the skill.- Optional companion files: supporting files (
example.json,style-guide.md, prompts, schemas) that the skill body can reference. The AI can read these when it follows the skill.
Doable stores skills in PostgreSQL (context_skills + context_skill_files) and materializes them to disk on demand at session start. Materialization is content-hashed, so repeated sessions for the same scope skip the rewrite. See services/api/src/ai/skills-materializer.ts.
Manifest format¶
Every skill has these fields (created via the UI or POST /:workspaceId/skills, defined in services/api/src/routes/skills.ts):
| Field | Type | Required | Description |
|---|---|---|---|
scope |
"workspace" \| "project" \| "user" |
yes | Visibility (see Scopes) |
skillName |
string, 1-200 chars | yes | Must start alphanumeric, may contain _, -, space |
description |
string, <= 500 chars | no | What the skill is for (the AI reads this to decide when to use it) |
skillContent |
string, non-empty | yes | The markdown body of SKILL.md |
autoInvoke |
boolean | no (default true) |
If false, the skill must be invoked explicitly |
projectId |
UUID | conditional | Required when scope === "project", forbidden otherwise |
The body itself is plain markdown. If you don't include frontmatter, Doable injects it for you:
---
name: "Commit messages"
description: "Use this when writing git commits. Apply our team's commit message style."
---
You can also write your own frontmatter; the materializer preserves it. The check is a simple regex match on ^---\s*\n[\s\S]*?\n---\s*\n at the top of the file.
Description is the most important field
The AI's host decides whether to load a skill based on the description (and the frontmatter that wraps the content). Write it as if you're explaining to a teammate: "Use this skill when ...". Vague descriptions get ignored.
Scopes¶
Three scopes determine who and where a skill is visible:
workspace: every member of the workspace gets it, in every project.project: only this project. Useful for project-specific style guides or migration playbooks.user: only the creating user. The API bindsuser_idto the authenticated caller, so even an admin cannot edit another user's personal skills (see the guard inroutes/skills.tsaround theuserscope).
When a chat session starts for (workspace, project, user), the materializer writes three sibling root folders and hands all three paths to the SDK:
<DOABLE_SKILLS_DIR>/
<workspaceId>/
workspace/<slug>/SKILL.md # all workspace-scoped skills
project/<projectId>/<slug>/... # only the active project's
user/<userId>/<slug>/... # only this user's
The slug is the lowercased, dash-separated form of skillName, capped at 64 chars.
Multi-file skills¶
Beyond SKILL.md, you can attach companion files: JSON schemas, code examples, longer references the skill body links to. The API surface:
GET /:workspaceId/skills/:id/files: list companion files (metadata only)POST /:workspaceId/skills/:id/files: create or update oneGET /:workspaceId/skills/:id/files/:path: read a single fileDELETE /:workspaceId/skills/:id/files/:path: remove one
File paths are validated by isSafeFilePath() in routes/skills.ts:
- Allowed characters:
a-z A-Z 0-9 . _ -plus/as a path separator. - No
..traversals, no leading/, noSKILL.md(reserved). - Max 512 chars in the path, max 2 MB per file.
A typical multi-file skill might look like:
my-skill/
SKILL.md # references the companions below
examples/
valid-input.json
invalid-input.json
schemas/
request.schema.json
In your SKILL.md, reference them with relative paths:
The AI can use its read_file tool to open these companions when it follows the skill.
How the AI invokes a skill¶
- Session start. The chat backend calls
materializeSkillsForSession({ workspaceId, projectId, userId }). This walkscontext_skillsfor the three relevant scopes, writes each skill's folder under$DOABLE_SKILLS_DIR, and returns the three root paths. - SDK discovery. The Copilot SDK recursively scans each
skillDirectoriesroot, parses every<slug>/SKILL.mdfrontmatter, and keeps the list in memory for the session. - Per-turn match. When the user sends a message, the SDK uses the skill descriptions as part of its system prompt. If a skill's description matches the request and
autoInvokeis true (the default), the SDK injects the skill body into the prompt. - Companion reads. If the skill body references companion files, the AI calls its
read_filetool with the relative path inside the skill folder.
Materialization is content-hashed (.manifest-hash file at each scope root). On a second session with the same skills, the materializer reads the hash, finds no change, and skips re-writing: fast and atomic.
Worked example: a "Migration playbook" skill¶
Suppose you have a Postgres migration system and want the AI to follow your house rules whenever it writes a .sql migration. Build a project-scoped skill.
1. Create the skill¶
curl -X POST "$API/workspaces/$WS/skills" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"scope": "project",
"projectId": "'$PROJECT'",
"skillName": "Postgres migration playbook",
"description": "Use when creating a new .sql migration. Apply our naming, idempotency, and rollback rules.",
"skillContent": "# Postgres migration playbook\n\nWhen creating a new migration:\n\n1. **File name**: `NNN_short_description.sql`, where NNN is one greater than the highest existing migration.\n2. **Idempotency**: every `CREATE` must be `IF NOT EXISTS`. Every `ALTER` must check for the column/constraint first via the catalog views in `examples/idempotent-alter.sql`.\n3. **Rollback**: include a `-- ROLLBACK:` comment block at the bottom showing the reverse operation.\n4. **No data migrations in DDL**: separate file, separate review.\n\nSee `examples/idempotent-alter.sql` for the pattern.",
"autoInvoke": true
}'
2. Attach a companion file¶
curl -X POST "$API/workspaces/$WS/skills/$SKILL_ID/files" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"filePath": "examples/idempotent-alter.sql",
"content": "-- Idempotent column add:\nDO $$ BEGIN\n IF NOT EXISTS (\n SELECT 1 FROM information_schema.columns\n WHERE table_name = '\''users'\'' AND column_name = '\''locale'\''\n ) THEN\n ALTER TABLE users ADD COLUMN locale text NOT NULL DEFAULT '\''en'\'';\n END IF;\nEND $$;\n"
}'
3. Test it¶
In the editor chat for that project:
"Add a migration that introduces a
last_seen_attimestamp column onusers."
The AI should pick up the playbook, name the file with the next number, use the idempotent pattern from the companion file, and include the rollback block.
Error handling¶
| Symptom | Likely cause |
|---|---|
400 - skillName must start alphanumeric... |
Your name starts with _, -, or whitespace |
400 - projectId is required for project-scoped skills |
Scope mismatch: supply projectId or change scope |
403 - Cannot edit another user's personal skill |
You're trying to PUT/DELETE a user-scoped skill owned by someone else |
400 - Invalid file path |
Companion file path failed isSafeFilePath() validation |
| AI ignores the skill | Description too vague, or the skill is project-scoped and you're chatting at workspace root |
| Skill folder missing from cache | Check API logs for [skills] Failed to materialize; a write may have failed silently |
Workspace vs platform scope¶
Custom skills always live at workspace/project/user scope; they're per-install. To ship a skill platform-wide (every Doable install gets it out of the box), you'd add it as a code-level prompt in the AI subsystem. That's a contribution to the doable repo, not a workspace setting; see Contributing Providers & Integrations for the PR flow.
Related¶
- MCP Servers: for stateful tools that go beyond instructions
- Knowledge Bases: for context the AI loads on every turn (not conditional like skills)
services/api/src/routes/skills.ts: the REST APIservices/api/src/ai/skills-materializer.ts: DB to disk