25 KiB
Skills
See also: CONTEXT.md · TOOLS.md
The Context Window Is Made of Skills
Every time an LLM runs, it receives a context window — the complete set of instructions and information it needs to reason and respond. In this system, the context window is broken up into units called skills.
╔══════════════════════════════════════════╗
║ CONTEXT WINDOW ║
║ ║
║ ┌────────────────────────────────────┐ ║
║ │ Skill 1: personality │ ║
║ │ │ ║
║ │ You speak concisely and directly. │ ║
║ │ You favor technical precision. │ ║
║ │ │ ║
║ │ tools: [my_name, my_npub] │ ║
║ └────────────────────────────────────┘ ║
║ ┌────────────────────────────────────┐ ║
║ │ Skill 2: chat │ ║
║ │ │ ║
║ │ Respond helpfully to the admin. │ ║
║ │ Use tools as needed. │ ║
║ │ │ ║
║ │ tools: [nostr_query, nostr_dm] │ ║
║ └────────────────────────────────────┘ ║
║ ║
╚══════════════════════════════════════════╝
Each skill is a block of instructions. The context window is a stack of these blocks. Different events produce different stacks — a DM conversation has one set of skills, a scheduled cron job has a completely different set.
A skill is a set of instructions for an LLM stored as a Nostr event. Skills teach an LLM how to accomplish tasks — the LLM reads the instructions, reasons about them, and uses tools to take action.
Think of it like a woodshop: a skill is knowing how to carve — technique, judgment, decision-making. A tool is the chisel. The skill never directly uses the chisel without the craftsperson (the LLM) in the loop.
Skills are portable, shareable, and discoverable as Nostr events. They are not specific to any single application — any app that can read Nostr events and call an LLM can use skills.
What Is a Skill?
A skill has two orthogonal properties:
- Triggers — A skill may have trigger tags, or not. If it has triggers, a runtime can fire it automatically when matching events occur. Triggered skills appear in the context window when their trigger matches (layer 1).
- References — A skill may be referenced by other skills via
{{skill_d_tag}}template variables, or not. If referenced, its content is included inside the referencing skill (layer 2).
These properties are independent. A skill can have triggers and be referenced. A skill can have triggers and never be referenced. A skill can have no triggers and only exist to be referenced. A skill can have neither (though that would be inert).
Skill Events
| Kind | Purpose | Replaceable? |
|---|---|---|
31123 |
Public skill definition | Yes, by d-tag |
31124 |
Private skill definition | Yes, by d-tag |
10123 |
Skill adoption list | Yes, single per pubkey |
Skill Content
The content field of a skill event IS the template — markdown instructions that go directly into the context window. No JSON wrapper. The description lives in a tag, not in content.
{
"kind": 31123,
"content": "system:\n# Spelling and Grammar Checker\n\nYou are a spelling and grammar checker.\n\n## Rules\n\n- Fix spelling errors\n- Fix grammar errors\n- Preserve original formatting\n- Return **ONLY** the corrected text, no explanations\n\nuser:\n{{message}}",
"tags": [
["d", "spellcheck"],
["description", "Check spelling and grammar"],
["trigger", "dm"],
["filter", "{\"from\":\"admin\"}"],
["llm", "openai/gpt-4o-mini, cheap"],
["temperature", "0"]
]
}
content— the template in markdown. May include{{...}}template variables andsystem:/user:role markers. This is what goes into the context window.["description", "..."]— human-readable description for discovery and UI display.- Each
["tag", "value"]is a separate tag on the Nostr event. - The
llmtag uses a CSS font-stack style fallback chain. See LLM Fallback Chain.
Two-Layer Context Model
When a skill executes, the context window is built in two layers:
- Layer 1: Triggered skills whose trigger matches the current event, ordered by their position in the adoption list (
10123). Only triggered skills can be in layer 1 — the trigger system is what puts them there. - Layer 2: Skills embedded inside layer 1 skills via
{{skill_d_tag}}template references. These are resolved inline, the same way tool-based template variables are resolved.
CONTEXT WINDOW — Admin DM arrives
═══════════════════════════════════════════════════
Layer 1: Triggered skills matching "dm/admin"
(ordered by adoption list)
┌─────────────────────────────────────────────────┐
│ TRIGGERED SKILL: personality │
│ trigger: dm, filter: {"from":"admin"} │
│ │
│ ┌───────────────────────────────────────┐ │
│ │ {{identity}} (adopted, no trigger) │ │
│ │ You are Didactyl. npub1abc...xyz │ │
│ └───────────────────────────────────────┘ │
│ │
│ You speak concisely and directly. │
│ You favor technical precision. │
│ You use dry humor sparingly. │
│ │
├─────────────────────────────────────────────────┤
│ TRIGGERED SKILL: chat │
│ trigger: dm, filter: {"from":"admin"} │
│ │
│ Respond helpfully. Use tools as needed. │
│ │
│ tools: [nostr_query, nostr_dm, nostr_post, │
│ memory_read, memory_write] │
│ │
├─────────────────────────────────────────────────┤
│ DM CONTENT (always last for dm triggers) │
│ │
│ "Hey, can you check who mentioned me today?" │
│ │
└─────────────────────────────────────────────────┘
CONTEXT WINDOW — Cron fires at noon
═══════════════════════════════════════════════════
Layer 1: Triggered skills matching "cron/0 12 * * *"
(ordered by adoption list)
┌─────────────────────────────────────────────────┐
│ TRIGGERED SKILL: readme-monitor │
│ trigger: cron, filter: 0 12 * * * │
│ │
│ ┌───────────────────────────────────────┐ │
│ │ {{identity}} (adopted, no trigger) │ │
│ │ You are Didactyl. npub1abc...xyz │ │
│ └───────────────────────────────────────┘ │
│ │
│ Check the readme at the configured URL. │
│ Compare with last known version in memory. │
│ If changed: post it and DM admin a summary. │
│ │
│ tools: [http_fetch, memory_read, │
│ memory_write, nostr_post, nostr_dm] │
│ │
├─────────────────────────────────────────────────┤
│ TRIGGERING EVENT │
│ │
│ {"type":"cron","filter":"0 12 * * *", │
│ "created_at":1742641200} │
│ │
└─────────────────────────────────────────────────┘
personality is NOT here — it has a dm trigger,
not a cron trigger, so it doesn't match layer 1.
identity IS here — but only as layer 2 inside
readme-monitor, because readme-monitor includes
{{identity}} in its template.
Adoption List (10123)
The adoption list serves two purposes:
- Registry — makes skills available for
{{skill_d_tag}}resolution (layer 2 inclusion) - Ordering — determines the order of layer 1 triggered skills in the context window
{
"kind": 10123,
"tags": [
["a", "31124:<pubkey>:identity"],
["a", "31124:<pubkey>:personality"],
["a", "31123:<pubkey>:chat"],
["a", "31123:<pubkey>:readme-monitor"]
]
}
identity— no trigger, adopted so triggered skills can include it via{{identity}}(layer 2)personality— has["trigger", "dm"], appears in layer 1 for DM events. Also referenceable via{{personality}}by other skills (layer 2).chat— has["trigger", "dm"], appears in layer 1 for DM events after personality (adoption list order)readme-monitor— has["trigger", "cron"], appears in layer 1 for cron events. Its template includes{{identity}}(layer 2).
Skills NOT in this list but with trigger tags are still armed — they fire when their trigger matches, but they execute in isolation (no layer 2 skill references available, only built-in variables).
Template Variables
Template variables resolve through tool execution or skill lookup.
When the engine encounters {{variable_name}}:
- Check if it matches a known tool — if so, execute the tool and insert the result
- Check if it matches an adopted skill's d-tag — if so, insert that skill's content (layer 2)
- If neither matches, resolve to empty (for portability)
Built-in Variables
| Variable | Resolution | Description |
|---|---|---|
{{agent_identity}} |
agent_identity tool |
Agent identity block |
{{admin_profile}} |
nostr_admin_profile tool |
Admin kind 0 profile |
{{admin_notes}} |
nostr_admin_notes tool |
Admin recent notes |
{{admin_relays}} |
nostr_admin_relays tool |
Admin relay list |
{{dm_history}} |
(expand directive) | Recent DM conversation |
{{message}} |
(built-in) | Current user message |
{{triggering_event}} |
trigger_event tool |
Triggering event JSON |
Skill Reference Variables
| Variable | Resolution | Description |
|---|---|---|
{{skill_d_tag}} |
Look up adopted skill by d-tag | Layer 2 skill inclusion |
Unknown variables resolve to empty values for portability.
Triggers
A skill with trigger tags can be fired automatically by a runtime when matching events occur.
Trigger Types
dm— Direct message receivedcron— Scheduled time expressionnostr-subscription— Nostr event matches a filterwebhook— HTTP request receivedchain— Another skill completed execution
Trigger Tags
| Tag | Required | Description |
|---|---|---|
trigger |
Yes | Trigger type |
filter |
Yes | Type-specific filter |
If a skill is in the adoption list, its triggers are active. There is no separate enabled flag — adoption IS enablement.
Execution Parameter Tags
These tags can appear at the top level of a skill event (defaults for any app) or on trigger-specific contexts (runtime overrides).
| Tag | Description |
|---|---|
llm |
Model spec with fallback chain (see below) |
max_tokens |
Max output tokens |
temperature |
Sampling temperature |
seed |
Optional deterministic seed |
LLM Fallback Chain
The llm tag uses a CSS font-stack style fallback chain. The runtime tries each entry in order, falling back to the next if the previous is unavailable.
Format: provider/model, provider/model, ..., capability_keyword
["llm", "anthropic/claude-sonnet-4-20250514, openai/gpt-4o-mini, cheap"]
This means:
- Try
anthropic/claude-sonnet-4-20250514first - If unavailable, try
openai/gpt-4o-mini - If unavailable, use whatever the runtime considers
cheap
Each entry can be:
provider/model— specific provider and model (e.g.,anthropic/claude-sonnet-4-20250514)model— model name only, use the default provider (e.g.,gpt-4o-mini)- Capability keyword — abstract tier the runtime resolves to its best available option
Capability keywords:
| Keyword | Meaning |
|---|---|
cheap |
Lowest cost model available |
fast |
Lowest latency model available |
best |
Highest capability model available |
default |
Use the agent/app default model |
Examples:
["llm", "openai/gpt-4o-mini"] -- specific model, no fallback
["llm", "openai/gpt-4o-mini, cheap"] -- try gpt-4o-mini, fall back to cheapest
["llm", "anthropic/claude-opus-4-20250514, openai/gpt-4o, best"] -- try opus, then gpt-4o, then best available
["llm", "fast"] -- just use the fastest available
["llm", "default"] -- use agent/app default
This is important for portability: a skill published with ["llm", "anthropic/claude-sonnet-4-20250514, cheap"] works on any runtime — if the runtime doesn't have Anthropic access, it falls back to its cheapest available model.
Execution Parameter Resolution
When a trigger fires:
- Start with agent/app defaults.
- Apply top-level execution tags from the skill event.
- Walk the
llmfallback chain until a usable model is found. - Apply trigger-specific overrides if present.
- Execute skill with those effective runtime settings.
- Restore defaults after the run.
Trigger Type Details
Each example below shows a complete skill event. Every ["tag", "value"] pair is a separate tag on the Nostr event.
dm
Fires when a direct message is received. filter is JSON with sender scope: {"from":"admin"}, {"from":"wot"}, or {"from":"any"}.
For DM triggers, the raw message content is always appended to the end of the context window.
{
"kind": 31123,
"content": "{{identity}}\n\nRespond helpfully to the admin.",
"tags": [
["d", "chat"],
["description", "Chat with admin"],
["trigger", "dm"],
["filter", "{\"from\":\"admin\"}"],
["llm", "default"],
["requires_skill", "identity"]
]
}
cron
Fires on a schedule. filter is a standard 5-field cron expression: minute hour day-of-month month day-of-week.
{
"kind": 31123,
"content": "{{identity}}\n\nCheck the readme at the configured URL. If changed, post it and DM admin.",
"tags": [
["d", "readme-monitor"],
["description", "Check readme for changes at noon"],
["trigger", "cron"],
["filter", "0 12 * * *"],
["llm", "openai/gpt-4o-mini, cheap"],
["max_tokens", "300"],
["requires_tool", "http_fetch"],
["requires_tool", "memory_read"],
["requires_tool", "memory_write"],
["requires_skill", "identity"]
]
}
nostr-subscription
Fires when a Nostr event matches a subscription filter. filter is a JSON-encoded Nostr subscription filter.
{
"kind": 31123,
"content": "{{identity}}\n\nWhen the triggering event mentions Bitcoin or Lightning, summarize and DM admin.",
"tags": [
["d", "mention-monitor"],
["description", "Monitor mentions and summarize"],
["trigger", "nostr-subscription"],
["filter", "{\"#p\":[\"<admin_pubkey>\"],\"kinds\":[1]}"],
["llm", "openai/gpt-4o-mini, cheap"],
["temperature", "0"],
["requires_tool", "nostr_query"],
["requires_tool", "nostr_dm"],
["requires_skill", "identity"]
]
}
webhook
Fires when an HTTP request is received. filter can be {} (match all).
{
"kind": 31123,
"content": "{{identity}}\n\nProcess the webhook payload and take appropriate action.",
"tags": [
["d", "webhook-handler"],
["description", "Process incoming webhook"],
["trigger", "webhook"],
["filter", "{}"]
]
}
chain
Fires when another skill completes execution. filter is the source skill's d tag.
{
"kind": 31123,
"content": "{{identity}}\n\nReview the output from the previous skill and DM admin a summary.",
"tags": [
["d", "readme-reviewer"],
["description", "Review results from readme monitor"],
["trigger", "chain"],
["filter", "readme-monitor"],
["llm", "openai/gpt-4o-mini, cheap"],
["requires_skill", "identity"]
]
}
Requirements Tags
Skills declare what they need to run. Apps use these tags to determine which skills are compatible with their available capabilities.
| Tag | Description |
|---|---|
requires_tool |
A tool that must be available for this skill to function |
requires_skill |
An adopted skill that must be present for {{...}} resolution |
optional_tool |
A tool that enhances the skill but is not required |
["requires_tool", "http_fetch"],
["requires_tool", "memory_read"],
["requires_tool", "memory_write"],
["requires_tool", "nostr_post"],
["requires_skill", "identity"],
["optional_tool", "nostr_dm"]
How Apps Use Requirements
App starts up
│
├─ Knows its available tools/capabilities
│
├─ Fetches user's adopted skills from 10123
│
├─ For each skill, checks requires_tool tags
│ ├─ All required tools available? → skill is usable
│ └─ Missing required tools? → skill is disabled
│
└─ Presents only usable skills to the user
Tool names in requirements tags are capability names, not implementation names. http_fetch is a capability — a C binary implements it with libcurl, a browser implements it with fetch(), a mobile app implements it with its HTTP library. The capability is the same; the implementation varies.
Private Skill Encoding (31124)
Private skills use NIP-44 encryption on event content.
Rules for kind 31124:
- Keep
dtag exposed so the event stays addressable/replaceable. - Move non-
dmetadata into plaintext payload before encryption. - Encrypt full payload with NIP-44 and store ciphertext in event
content. - On receive: resolve by
d, decryptcontent, then read content + private tags.
Private Skill Event (on relay)
{
"kind": 31124,
"content": "<nip44-ciphertext>",
"tags": [
["d", "mention-monitor"]
]
}
Decrypted Private Payload (application-level JSON)
{
"content": "{{identity}}\n\nWhen {{triggering_event}} includes Bitcoin or Lightning, summarize and DM admin.",
"private_tags": [
["description", "Monitor mentions and DM summaries"],
["scope", "private"],
["trigger", "nostr-subscription"],
["filter", "{\"#p\":[\"<admin_pubkey>\"],\"kinds\":[1]}"],
["llm", "openai/gpt-4o-mini, cheap"],
["temperature", "0"],
["requires_tool", "nostr_query"],
["requires_tool", "nostr_dm"],
["requires_skill", "identity"]
]
}
Execution Flow
Trigger event occurs (DM, cron, subscription, webhook, chain)
│
├─ Walk adoption list (10123)
│ │
│ ├─ For each skill whose trigger matches this event:
│ │ ├─ Resolve template variables (tools + skill references)
│ │ └─ Add to context (layer 1)
│ │
│ └─ Skills whose trigger does NOT match: skip
│
├─ Append triggering event payload
│ └─ For DM triggers: always append raw message content
│
├─ Apply execution parameters (llm, temperature, max_tokens, tools)
│
└─ Send to LLM → multi-turn tool loop → response
Limits and Safety
| Limit | Default | Description |
|---|---|---|
| Max concurrent triggers | 16 | Prevents resource exhaustion |
| Trigger cooldown | 60s per skill | Prevents rapid-fire execution |
| LLM action rate limit | 10/min | Prevents runaway LLM costs |
Storage on Nostr
| Data | Storage |
|---|---|
| Skills | Kind 31123/31124 events |
| Adopted skills | Kind 10123 event |
| Trigger definitions + execution params | Tags on skill events |
| Requirements declarations | Tags on skill events |
Portability
Skills are Nostr events. Any application that can read Nostr events and call a skill which will call an llm. Skills are not specific to Didactyl or any single runtime.
Use Cases Beyond Didactyl
- Word processor — "Check spelling and grammar" button triggers a spellcheck skill
- Browser extension — Highlight text, run a summarization skill
- Mobile app — Voice input triggers a transcription skill
- Browser-based agent — Same agent, different runtime, different available tools
Portability Guidelines
| Guideline | Rationale |
|---|---|
Use {{message}} for user input |
Universal — every app has user input |
Declare requirements via requires_tool / requires_skill tags |
Lets apps filter to compatible skills |
| Put default execution params as top-level tags | Any app can read llm, temperature, etc. |
| Keep trigger tags as optional runtime hints | Apps without trigger systems ignore them |
| Resolve unknown variables to empty | Ensures graceful degradation |
| Prefer self-contained skills for maximum portability | Skills with {{skill_d_tag}} references need the adoption ecosystem |
| Treat tool names as capabilities, not implementations | http_fetch works in C, browser, mobile — same capability, different implementation |
A skill should still be useful even when some variables, tools, or referenced skills are unavailable.
Related Documentation
- Tool architecture and complete tool catalog:TOOLS.md
- Context assembly model:CONTEXT.md
- Project overview/runtime behavior:README.md