Skip to content

Skills

A skill is a markdown knowledge module (frontmatter + body) that the agent can pull into its system prompt to gain specialized domain knowledge. Skills are not connector tools — they don't execute anything. They're context the agent can opt into, scoped by a per-conversation prompt-character budget.

If you've seen Anthropic's Claude Skills, the model is the same: bundles of markdown that teach the agent how to handle a specific domain. Agentcy adds a discovery API, an enablement budget, an approval gate, and a community registry.

Skills vs connector tools. Two different systems with two different meta-tool sets:

Connector tools (Concept: Skills here is for skills)Skills (this page)
What it isCallable functions (github.list_pulls, k8s.restart_deployment)Markdown knowledge bundles
Meta-toolslist_connectors, search_connector_tools, execute_connector_tool, request_connector_accesssearch_skills, enable_skill, list_enabled_skills, disable_skill
EffectRuns code, gets dataAdds text to system prompt
BudgetNone (4 meta-tools always small)Char budget per conversation
LifecyclePermanent until source disabledEnabled per session, can be disabled
SeeConcept: ConnectorsThis page

What a skill looks like

A skill is markdown with YAML frontmatter:

markdown
---
name: PostgreSQL Health Triage
slug: postgres.health-triage
description: Diagnose slow queries, lock waits, and replication lag in PostgreSQL.
version: 1.2.0
author: agentcy/community
tags: [database, postgres, sre]
---

# PostgreSQL Health Triage

When investigating a slow Postgres instance, work through these checks in order:

## 1. Active queries
Use `pg_stat_activity` filtered to `state='active'` and order by duration desc...

## 2. Lock contention
Check `pg_locks` for blocked queries. The classic pattern is...

## 3. Replication lag
On the primary: `SELECT * FROM pg_stat_replication`. Lag over 10s suggests...

## 4. When to escalate
- Locks held > 5 min on a hot table
- Replication lag growing unbounded
- Connection count saturating max_connections

The body is prose the LLM reads as context. It's not code. The frontmatter determines how the catalog finds and ranks it.

How skills change a conversation

When a skill is enabled, its full markdown content is appended to the system prompt as a <skill name="…">…</skill> block. The agent now "knows" that domain for the rest of the turn-loop, until the skill is disabled or the conversation ends.

[system]   org policies + active realm + tool-catalog instructions
[system]   <memory>...recalled facts...</memory>
[system]   <graph_context>...</graph_context>
[system]   <skill name="PostgreSQL Health Triage">
             # PostgreSQL Health Triage
             When investigating a slow Postgres instance...
             ...
           </skill>
[system]   <skill name="Incident Comms Style Guide">
             ...
           </skill>
[user]     ...prior turns...
[user]     "checkout-svc is slow, what's wrong?"

Two crucial properties:

  1. Skills are full-text injected. They consume real prompt tokens. That's why the budget exists.
  2. The agent can discover and enable them mid-conversation. Skills aren't pre-loaded — the LLM uses the search/enable meta-tools when a domain it doesn't have shows up.

The four skill meta-tools

Available to the agent on top of the four connector meta-tools:

rust
search_skills(query?, tags?)            // returns ranked list of available skills
enable_skill(skill_slug, reason?)       // adds skill content to system prompt
list_enabled_skills()                   // current state + budget usage
disable_skill(skill_slug)               // removes from prompt, frees budget

A typical conversation:

text
User: "the prod Postgres is responding slowly, can you triage it?"

LLM → search_skills({ query: "postgres slow query triage" })
        ↘ returns: [postgres.health-triage (v1.2, 2,134 chars), …]

LLM → enable_skill({ skill_slug: "postgres.health-triage", reason: "user reports slow Postgres" })
        ↘ approval gate (if policy requires) → SSE: approval_required
        ↘ user approves → skill content added to next system prompt
        ↘ SSE: skills_state_changed { budget_used: 2134/16000 }

[next turn — system prompt now contains the skill body]

LLM → search_connector_tools({ query: "pg_stat_activity" })
        ↘ kubernetes.exec_in_pod, sql.run_query → uses sql.run_query
LLM → execute_connector_tool({ connector: "sql", tool: "run_query", … })
        ↘ result: 5 long-running queries
LLM → composes a triaged answer following the skill's checklist.

LLM → disable_skill({ skill_slug: "postgres.health-triage" })  // frees budget

The prompt budget

The single most important constraint. Defaults:

SettingDefaultNotes
skills_max_prompt_chars16 000Per-conversation hard cap on Σ enabled.content_length
skills_dynamic_search_enabledfalseWhether registry tools (search_skills_registry, fetch_registry_skill) are available
skill_enablement_policyautoauto · prompt-user · admin-only — controls approval flow
skills_dynamic_search_policyprompt-userApproval policy specifically for registry-fetched skills

enable_skill returns an error if current_chars + new_skill.content_length > max_prompt_chars. The agent is told the limit and can disable_skill to make room. This is the whole point — skills are powerful but expensive, so the agent has to be deliberate.

The frontend tracks budget live via the skills_state_changed SSE event:

json
event: skills_state_changed
data: {
  "enabled_skills": [
    { "name": "PostgreSQL Health Triage", "slug": "postgres.health-triage",
      "content_length": 2134, "source_type": "catalog" }
  ],
  "budget_used_chars":  2134,
  "budget_max_chars":  16000
}

The UI's chat panel shows a "Skills (2,134 / 16,000)" pill that updates in real time.

Skill scope: org vs user

Each skill row has a scope:

  • org — visible to everyone in the organization. Default for most skills.
  • user — visible only to the user who created it. Useful for personal cheatsheets, idiosyncratic preferences, work-in-progress drafts.

The list_skills meta-tool returns both by default. UI My skills vs Org skills filters on this.

Where skills come from

Five source_type values cover the lifecycle:

manual — written in the editor

The Skills page has a markdown editor. Save → it's a skill. No external dependency, just a row in skills with the markdown content.

bash
curl -X POST "$API/api/v1/skills" \
  -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{
    "name": "Incident Comms Style Guide",
    "slug": "comms.incident-style",
    "description": "Tone and structure for customer-facing incident updates.",
    "tags": ["sre", "comms"],
    "scope": "org",
    "content": "---\nname: Incident Comms Style Guide\n…\n---\n\n# Style guide\n\n…"
  }'

github — pulled from a Git repo

A skill_source of type github watches a path in a repo (e.g. acme/runbooks path: skills) and parses every .md file into a skill row.

bash
curl -X POST "$API/api/v1/skills/sources" \
  -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{
    "name": "acme runbook skills",
    "source_type": "github",
    "config": {
      "repo": "acme/runbooks",
      "ref": "main",
      "path": "skills",
      "sync_policy": "auto",
      "sync_interval_minutes": 360
    }
  }'

Sync triggers:

  • Manual: POST /skills/sources/{id}/sync
  • Auto: sync_policy: auto re-fetches every sync_interval_minutes
  • Bulk: POST /skills/sources/sync-stale re-syncs anything past TTL

url — fetched from a single URL

For one-off remote skills (a Gist, a public runbook). Re-fetched on sync.

json
{ "source_type": "url", "config": { "url": "https://example.com/skill.md" } }

marketplace — installed from skills.sh

The Browse tab on the Skills page (and the search_skills_registry meta-tool) talk to the external skills.sh registry. Curated community skills, ranked by trending and quality.

bash
# Search the registry
curl "$API/api/v1/skills/registry/search?q=postgres" \
  -H "authorization: Bearer $TOKEN"

# Install — same shape as a manual skill, content from the registry
curl -X POST "$API/api/v1/skills" \
  -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{ "source_type": "marketplace", "source_url": "https://skills.sh/…", … }'

Dynamic registry fetch (no install)

When skills_dynamic_search_enabled is on, the agent gains two extra meta-tools:

rust
search_skills_registry(query, limit?)   // searches skills.sh live
fetch_registry_skill(source, skill_id?, reason?)  // downloads + activates *without* installing

fetch_registry_skill is the magic move: the skill is held in memory for this conversation only, doesn't pollute the org's installed list, and counts against the same prompt budget. Great for one-off "I need this domain knowledge for the next 5 minutes" moments. Behavior is gated by skills_dynamic_search_policy (default: prompt-user for approval).

The dynamic skill becomes a (name, content) pair stored in a dynamic_registry_skills RwLock<Vec> for the conversation. It contributes to the budget but never persists.

Approval flow

enable_skill (and fetch_registry_skill) can require approval depending on policy:

skill_enablement_policyBehavior
autoSkills enable instantly with no human gate
prompt-userSSE approval_required { type: "skill_enablement", skill: {…} } — UI shows confirm/deny card
admin-onlyAlways denies for non-admins. Agent gets a clear tool_error telling it to ask for help

Approval lives in the same ApprovalRegistry as connector-tool approvals, with ApprovalType::SkillEnablement. Same Tokio oneshot pattern; same UI component family.

Storage

Two tables:

skills — one row per skill. Columns include:

ColumnNotes
id, organization_idUUID primary key + tenant scope
user_idNULL for org-scoped, set for user-scoped
name, slug, description, version, author, tags[]Metadata
contentFull markdown body (frontmatter + prose)
content_lengthPre-computed char count — drives budget math
content_hashStable hash of body, used to detect changes on sync
source_typemanual · github · url · marketplace
source_url, source_ref, source_idProvenance pointers
enabledOrg-level "is this skill installable at all" flag

skill_sources — one row per upstream source (GitHub repo, URL, marketplace base):

ColumnNotes
id, organization_id, name
source_typegithub · marketplace · url
config (JSONB){repo, path, ref, sync_policy, sync_interval_minutes} for github, {url} for url, {base_url} for marketplace
enabled, last_sync_at, last_sync_status, last_sync_errorSync state

Code: backend/crates/agentcy-api/src/routes/skills.rs (~1,260 lines). Trait: agentcy_chat::SkillsCatalog.

How skills plug into the agent loop

A skill itself doesn't execute. But its enablement is a tool call, so it goes through the standard agent-loop gates:

LLM emits tool_call: enable_skill(skill_slug="postgres.health-triage")

Route → SkillsToolRegistry (because tool name matches)

Policy gate (kg-policy)  — rare; usually skill_enablement_policy handles this

Approval gate (if policy != "auto")
    → SSE: approval_required { type: "skill_enablement", skill: {…} }
    → user POSTs /approvals/:id { approved: true }

SkillsCatalog.enable_skill(slug)
    → checks budget — fails fast with KgError if over
    → adds slug to enabled_slugs set

SSE: skills_state_changed { enabled_skills, budget_used, budget_max }

Next LLM turn: get_enabled_skills_content() returns the (name, content) pairs,
the chat route prepends them as <skill name="…">…</skill> system blocks.

The next LLM turn sees the prompt enriched with skill content. No connector tools were called. Skills are pure context.

Permissions

Two relevant permissions in kg-core::tenant::permissions:

  • skills.read — list skills, see definitions, run search_skills
  • skills.write — create / update / delete skills + sources, install from registry

Org settings:

  • Settings → Skills → Prompt budget (skills_max_prompt_chars)
  • Settings → Skills → Enablement policy (skill_enablement_policy)
  • Settings → Skills → Dynamic search (skills_dynamic_search_enabled + policy)

REST API surface

Full reference: REST API: Skills.

MethodPathPurpose
GET/api/v1/skillsList skills (q, tags, scope=org|user, source_id)
POST/api/v1/skillsCreate a skill
POST/api/v1/skills/parseParse a markdown body — extracts frontmatter into a draft
GET/api/v1/skills/{id}Get a skill
PUT/api/v1/skills/{id}Update
DELETE/api/v1/skills/{id}Delete
GET/api/v1/skills/sourcesList sources
POST/api/v1/skills/sourcesCreate a source
PUT/api/v1/skills/sources/{id}Update
DELETE/api/v1/skills/sources/{id}Delete
POST/api/v1/skills/sources/{id}/syncForce sync one
POST/api/v1/skills/sources/sync-staleBulk re-sync past TTL
GET/api/v1/skills/registry/searchskills.sh search
GET/api/v1/skills/registry/trendingskills.sh trending
GET/api/v1/skills/registry/metadataskills.sh single-skill metadata

Gotchas

  • Skills don't execute. If a skill says "run pg_stat_activity," the agent still has to call a connector tool (sql.run_query, etc.) to actually do it. The skill is the playbook; the connector is the hands.
  • Char count is not token count. The budget is in characters because that's cheap to compute. A 16k char budget is roughly 4–5k tokens depending on content — a meaningful chunk of context.
  • Two skills with the same slug in the same org → uniqueness violation on insert. Pick a namespacing convention up-front (team.purpose, e.g. sre.k8s-triage).
  • Frontmatter is the contract. If description is empty or vague, search_skills won't rank it. Treat the description as marketing copy aimed at the LLM.
  • Markdown body length matters more than you think. A 6,000-char skill consumes ~38% of the default budget by itself. Trim aggressively.
  • Dynamic registry skills don't persist. Refreshing the conversation kills them. If a registry skill keeps showing up in agent searches, install it.

Next

Built by AgentcyLabs. For in-house deployment or Agentcy Cloud (PaaS) access, visit agentcylabs.com.