Writing Skills
Skills are markdown files that teach the agent how to handle specific tasks. They are the simplest way to extend Denkeeper.
Anatomy of a skill
Create a file in ~/.denkeeper/skills/:
+++
name = "expense-tracker"
description = "Track and categorize expenses"
version = "1.0.0"
triggers = ["command:expense"]
[requires]
tools = []
+++
## Instructions
When the user reports an expense:
1. Extract the amount, currency, and category
2. Confirm the details with the user
3. Store it in your memory for the weekly summary
## Categories
- Food & dining
- Transport
- Entertainment
- Utilities
- Other
## Response format
Always acknowledge with: "Logged: $AMOUNT for CATEGORY"
Frontmatter fields
| Field | Required | Description |
|---|---|---|
name | Yes | Unique identifier |
description | Yes | What the skill does (shown in Telegram command menu for command triggers) |
version | No | Semantic version |
triggers | No | When to activate (see below) |
requires.tools | No | MCP tools this skill needs. The skill is skipped while any of them is unavailable, and an active declaration narrows the tools the model is shown (see below) |
max_tool_rounds | No | Cap on tool-call rounds for turns this skill drives (command or schedule trigger). Only lowers the agent’s limit, never raises it; must be ≥ 0 |
Trigger types
command:name— activates when the user’s message starts with/nameor!name(case-insensitive), on any adapter or channel — Telegram, Discord, the web dashboard, or the REST chat APIschedule:<name>— marks the skill as scheduler-driven. Timing comes from a[[schedules]]entry whoseskillfield names this skill; without one, the skill never fires. Skill writes (REST, MCP, or the agent itself) reject a newly added<name>that isn’t an existing schedule; triggers the skill already carries, and a bareschedule:, are accepted- No triggers — ambient: always included in the system prompt, and matched on every turn
The distinction between ambient and scheduled matters for max_tool_rounds: the cap applies only when a single skill explicitly drives the turn (a command match, or a schedule naming it). An ambient skill matches every message, so capping on it would throttle unrelated conversation — it is deliberately exempt.
Tool requirements
A skill that names tools in requires.tools is only injected while every one of them is registered:
requires.tools = ["gmail_list", "gmail_send"]
If the MCP server hosting gmail_list is disconnected or disabled, the skill goes quiet — it is left out of the system prompt entirely rather than instructing the agent to call a tool that isn’t there. Availability is re-checked on every message, so the skill comes back on its own the moment the server reconnects. No restart or config reload is involved.
Notes:
- Names are matched exactly as the MCP server advertises them (same as auto-approve rules). A typo means the skill never activates.
- This applies to scheduled skills too: a run whose tools are missing does nothing and logs a warning, rather than half-executing.
- Deactivation and reactivation are logged once per change, not once per message.
- A skill with no
requires.toolsis always active — the field is opt-in.
What the model is shown
The declaration also narrows the tool list sent to the model. On a turn where at least one active skill declares requires.tools, only the union of those declarations is advertised — everything else is hidden for that turn, so a focused skill is not distracted by the agent’s entire tool surface.
- Declare everything the skill calls. A tool you forget is invisible for that turn, not merely undocumented.
- A turn where no active skill declares anything advertises every tool, as before. The narrowing is opt-in per turn, the same way the field is.
- An ambient skill (no triggers) matches every message, so a declaration on one narrows ordinary conversation too. Put
requires.toolson command- or schedule-driven skills unless that is what you want. - The agent’s own management tools (skills, persona, schedules, KV) are never hidden — otherwise a mistyped declaration would be unfixable from the inside.
Agent-specific skills
Place skills in an agent’s persona directory to scope them:
~/.denkeeper/agents/work-assistant/skills/standup.md
Agent-specific skills are merged with global skills. Same-name agent skills override global ones.
Testing a skill
The fastest loop is a dry run — it executes the turn now and shows you the transcript without storing anything:
curl -X POST -H "Authorization: Bearer dk_..." \
-H "Content-Type: application/json" \
-d '{"message": "coffee, $12"}' \
https://localhost:8080/api/v1/skills/default/expense-tracker/dry-run
The dashboard’s Skills page exposes the same thing with a transcript panel. This matters most for scheduled skills: without it, testing a 7am briefing means waiting until 7am.
A dry run persists nothing and suppresses non-read-only tools, but it does spend real tokens and does let read-only tools hit the network. See Dry Runs & Previews for exactly what is and is not isolated.
To test the real path end to end:
- Create the skill file
- Restart Denkeeper (skills are loaded at startup)
- If the skill has a
command:trigger, send the command in Telegram - Check that the agent follows the instructions