Beta . Ortha CMS is in public beta. Every plan is free until it ends.
Features Ortha CMS on GitHub

Skills

Copilot plugin @orthacms/copilot-server@orthacms/copilot-admin

Named instruction packets, declared in code or authored in the admin.

Documents 0.13.1 Updated Edit this page Report a problem

On this page

A skill is a named instruction packet plus a description of when it applies — your house style, editorial rules, the shape of a product description.

SourceScopeWhoEditable in the admin
Code, on CopilotPluginEvery workspaceAn operator, at deployNo — read-only
Admin → Agents → Skills (/workspaces/:id/agents/skills)One workspaceAn Admin (copilot:skills:manage)Yes

Declare one in code

// apps/server/src/plugins.ts
import { readFileSync } from 'node:fs';
import { join } from 'node:path';

CopilotPlugin({
    providers: copilotProviders(config),
    skills: [
        {
            name: 'house-style',
            title: 'House style',
            description: 'How we write product copy: sentence case, second person.',
            instructions: readFileSync(join(__dirname, 'skills/house-style.md'), 'utf8'),
            mode: 'always' // omit for 'manual': only when someone attaches it
        }
    ],
    config: config.plugins.copilot
});
<!-- skills/house-style.md -->
- Headlines in sentence case. No exclamation marks.
- Address the reader as "you".
- Standfirst: one sentence, under 25 words, no jargon.

Code skills are validated at construction — a duplicate or malformed one fails boot.

FieldRule
nameLowercase letters, digits and hyphens (house-style); ≤ 64
titleShown in the picker; ≤ 80
descriptionWritten for the model — when to use it; ≤ 240
instructionsThe body; ≤ 8,000
modemanual (default) or always

Code wins a name collision. The admin refuses a name code already holds, so a collision only happens when a deploy takes over a name.

Manage them over HTTP

Session-authenticated admin routes, scoped by X-Workspace-Id:

RouteNeedsDoes
GET /api/copilot/skillscopilot:useSkills available here, without instructions
GET /api/copilot/skills/managecopilot:skills:manageEvery skill, disabled and code-defined included
GET /api/copilot/skills/:idcopilot:skills:manageOne skill, with instructions
POST /api/copilot/skillscopilot:skills:manageCreate; 409 if either source holds the name
PATCH /api/copilot/skills/:idcopilot:skills:manageEdit, including enabled and mode
DELETE /api/copilot/skills/:idcopilot:skills:manageDelete (204); past turns keep their snapshot
Example language
curl -b cookies.txt -X POST https://cms.example.com/api/copilot/skills \
-H 'X-Workspace-Id: 7f1c…' \
-H 'Content-Type: application/json' \
-d '{
  "name": "product-copy",
  "title": "Product copy",
  "description": "Use when writing or editing a product description.",
  "instructions": "Lead with the benefit. Three short paragraphs. End with the price.",
  "mode": "manual"
}'
curl -b cookies.txt -X PATCH https://cms.example.com/api/copilot/skills/9a3f… \
-H 'X-Workspace-Id: 7f1c…' \
-H 'Content-Type: application/json' \
-d '{"enabled": false}'
curl -b cookies.txt https://cms.example.com/api/copilot/skills \
-H 'X-Workspace-Id: 7f1c…'
// POST → 201
{
    "id": "9a3f…",
    "name": "product-copy",
    "title": "Product copy",
    "description": "Use when writing or editing a product description.",
    "instructions": "Lead with the benefit. Three short paragraphs. End with the price.",
    "mode": "manual",
    "enabled": true,
    "source": "cms",
    "createdBy": "a51e…",
    "createdAt": "2026-09-30T10:20:00.000Z",
    "updatedAt": "2026-09-30T10:20:00.000Z"
}
// GET /api/copilot/skills → 200
[
    { "id": null, "name": "house-style", "title": "House style", "description": "…",
      "mode": "always", "source": "code", "enabled": true, "editable": false },
    { "id": "9a3f…", "name": "product-copy", "title": "Product copy", "description": "…",
      "mode": "manual", "source": "cms", "enabled": true, "editable": true }
]
// 409 — name taken
{ "message": "\"house-style\" is the name of a skill defined in this deployment's configuration. Choose another name.", "error": "Conflict", "statusCode": 409 }
  • An empty PATCH body is a 400.
  • The Skills page lists each skill with what it Applies to — Every chat (always) or When turned on (manual) — and an Available switch on the row (enabled). A code skill says From code: its switch is drawn but cannot be changed, and it has no menu. New skill and Edit skill open a sheet over the list.

Three ways a skill reaches a run

All three are the system prompt.

ChannelEffect
mode: 'always'In force for every run in the workspace, whatever the client sends
Attached for a turnIn force for that turn — up to 3
Every other enabled skillAdvertised as - name — title: description, up to 25

Attaching one — what the composer sends when you pick a skill:

// POST /api/copilot/runs
{ "message": "Rewrite the Aurora lamp description.", "skills": [{ "name": "product-copy" }] }

The third channel lets the model recommend a skill it wasn’t given (“want me to use product-copy?”) without its instructions costing context every run.

  • Skill bodies sit after the security rules (which they cannot override) and before the style rules (which they refine).
  • A run names skills; it never carries their text. Nobody with copilot:use can write their own system prompt.
  • Each turn records a snapshot of the skills it ran with.

No tool delivers a skill

A copilot_skill_read tool was considered and rejected. It would need an exception in the untrusted-content fence, or instructions labelled as inert data. Skills arrive through the system prompt or not at all.

Skills are instructions, not capabilities

A skill cannot add a tool, widen a permission, or reach data the caller could not. A skill saying “you may publish” changes nothing — there is no publish tool, and the permission check does not read the prompt.