Skip to content

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 a name and a description. 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 binds user_id to the authenticated caller, so even an admin cannot edit another user's personal skills (see the guard in routes/skills.ts around the user scope).

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 one
  • GET /:workspaceId/skills/:id/files/:path: read a single file
  • DELETE /: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 /, no SKILL.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:

## Examples

See `examples/valid-input.json` for a working request shape.

The AI can use its read_file tool to open these companions when it follows the skill.

How the AI invokes a skill

  1. Session start. The chat backend calls materializeSkillsForSession({ workspaceId, projectId, userId }). This walks context_skills for the three relevant scopes, writes each skill's folder under $DOABLE_SKILLS_DIR, and returns the three root paths.
  2. SDK discovery. The Copilot SDK recursively scans each skillDirectories root, parses every <slug>/SKILL.md frontmatter, and keeps the list in memory for the session.
  3. 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 autoInvoke is true (the default), the SDK injects the skill body into the prompt.
  4. Companion reads. If the skill body references companion files, the AI calls its read_file tool 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_at timestamp column on users."

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.