Appearance
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 is Callable functions (github.list_pulls, k8s.restart_deployment) Markdown knowledge bundles Meta-tools list_connectors,search_connector_tools,execute_connector_tool,request_connector_accesssearch_skills,enable_skill,list_enabled_skills,disable_skillEffect Runs code, gets data Adds text to system prompt Budget None (4 meta-tools always small) Char budget per conversation Lifecycle Permanent until source disabled Enabled per session, can be disabled See Concept: Connectors This 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_connectionsThe 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:
- Skills are full-text injected. They consume real prompt tokens. That's why the budget exists.
- 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 budgetA 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 budgetThe prompt budget
The single most important constraint. Defaults:
| Setting | Default | Notes |
|---|---|---|
skills_max_prompt_chars | 16 000 | Per-conversation hard cap on Σ enabled.content_length |
skills_dynamic_search_enabled | false | Whether registry tools (search_skills_registry, fetch_registry_skill) are available |
skill_enablement_policy | auto | auto · prompt-user · admin-only — controls approval flow |
skills_dynamic_search_policy | prompt-user | Approval 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: autore-fetches everysync_interval_minutes - Bulk:
POST /skills/sources/sync-stalere-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* installingfetch_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_policy | Behavior |
|---|---|
auto | Skills enable instantly with no human gate |
prompt-user | SSE approval_required { type: "skill_enablement", skill: {…} } — UI shows confirm/deny card |
admin-only | Always 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:
| Column | Notes |
|---|---|
id, organization_id | UUID primary key + tenant scope |
user_id | NULL for org-scoped, set for user-scoped |
name, slug, description, version, author, tags[] | Metadata |
content | Full markdown body (frontmatter + prose) |
content_length | Pre-computed char count — drives budget math |
content_hash | Stable hash of body, used to detect changes on sync |
source_type | manual · github · url · marketplace |
source_url, source_ref, source_id | Provenance pointers |
enabled | Org-level "is this skill installable at all" flag |
skill_sources — one row per upstream source (GitHub repo, URL, marketplace base):
| Column | Notes |
|---|---|
id, organization_id, name | |
source_type | github · 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_error | Sync 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, runsearch_skillsskills.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.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/skills | List skills (q, tags, scope=org|user, source_id) |
| POST | /api/v1/skills | Create a skill |
| POST | /api/v1/skills/parse | Parse 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/sources | List sources |
| POST | /api/v1/skills/sources | Create a source |
| PUT | /api/v1/skills/sources/{id} | Update |
| DELETE | /api/v1/skills/sources/{id} | Delete |
| POST | /api/v1/skills/sources/{id}/sync | Force sync one |
| POST | /api/v1/skills/sources/sync-stale | Bulk re-sync past TTL |
| GET | /api/v1/skills/registry/search | skills.sh search |
| GET | /api/v1/skills/registry/trending | skills.sh trending |
| GET | /api/v1/skills/registry/metadata | skills.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
descriptionis empty or vague,search_skillswon'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
- How-To: Tool Calling & the Catalog — the connector meta-tools (different from skill meta-tools)
- Concept: Agent Loop — where skill enablement plugs into the gate sequence
- Concept: Connectors — what runs when a skill says "run query X"
- Concept: Memory System — the other source of system-prompt context (memory recall + graph)