Skills
Skills are reusable instruction packages that give the agent specialized knowledge and procedures. They are loaded at runtime from directories and exposed to the LLM as the use_skill built-in tool.
How skills work
Skills follow a two-phase lifecycle: discovery at startup, then activation on demand.
Phase 1: Discovery (at startup)
When the application starts, initializeSkillRegistry() queries configured skill sources (built-in filesystem paths or injected sources). For each skill found:
- Skill frontmatter is parsed to extract metadata (name, description, tags, model preference) — the body is loaded lazily on demand.
- A
SkillRecordis created with metadata and source provenance. - Skills with duplicate names are resolved with explicit source-ordered precedence.
After discovery, buildSkillCatalog(metadata) generates a compact text block listing every skill's name, description, and tags. This catalog is composed into the system prompt, so the LLM always knows what skills are available.
initializeSkillRegistry(options)
Frontmatter parsed for each skill
Yields SkillRecord objects without bodies
buildSkillCatalog(metadata)
Produces catalog lines such as "- docker-ops: Docker container management [docker, deployment]"
Catalog composed into system prompt
Phase 2: Activation (when invoked)
There are two distinct activation paths:
- Slash command path (CLI only): The user types
/skillname args. The input is parsed, the skill body is loaded from disk, arguments are substituted,@pathreferences are resolved, and the constructed prompt is sent torunAgentLoop. - use_skill tool path (all adapters): The AI decides to use a skill based on the catalog in its system prompt. It calls the
use_skilltool. The tool handler loads the body, substitutes arguments, and returns the content as a tool result string.
See Two invocation paths for a detailed walkthrough of each path.
Loading skills
Automatic discovery
Skills are loaded automatically when you specify skill names:
import { askSeepient } from "seepient";
const result = await askSeepient("Deploy the staging environment", {
skills: ["docker-ops"],
tools: ["core", "comm", "advanced"],
});With createSeepient
import { createSeepient } from "seepient";
const agent = await createSeepient({
skills: ["docker-ops", "code-review"],
tools: ["core", "comm"],
});
const reply = await agent.chat("Review my latest commit");Skill sources
In addition to ambient filesystem discovery, Seepient allows embedders to inject external skill sources via the sources option on askSeepient() and createSeepient(), or pass inline skill definitions via the skills option. This enables serverless execution, multi-tenant skill partitioning, and remote database catalogs.
The SkillSource and SkillStore contracts
import type { SkillRecord, SkillSource, SkillStore, SkillLiteral } from "seepient";
// Any object implementing list() satisfies SkillSource
const dbSource: SkillSource = {
async list(): Promise<SkillRecord[]> {
return [
{
name: "customer-lookup",
content: "---\nname: customer-lookup\ndescription: Query customer CRM\n---\nProcedure...",
source: "crm-db", // Attribution label
},
];
},
};Inline skill literals
For serverless functions, tests, or zero-infrastructure workflows, you can pass skill literals directly using skills:
import { askSeepient } from "seepient";
const result = await askSeepient("Summarize the latest report", {
skills: [
{
name: "summarize",
content: "---\nname: summarize\ndescription: Summarize text succinctly\n---\nSummary instructions...",
},
],
});Inline literals are automatically synthesized into a source attributed as "inline" and appended after any explicit sources. They always shadow skills of the same name.
Composition semantics
When constructing the agent's skill catalog, sources are composed in order with last-wins shadowing:
| Tenancy Mode | Composition Pipeline | Behavior |
|---|---|---|
single (default) | [new FsSkillSources(cwd), ...(sources ?? []), inline?] | Built-in filesystem layers load first. Injected sources override filesystem skills of the same name. Inline literals shadow everything. Note: Passing sources without explicitly setting tenancy: "single" automatically upgrades tenancy mode to multi; pass tenancy: "single" explicitly to preserve ambient filesystem discovery alongside injected sources. Passing inline skills literals alone does not upgrade tenancy mode to multi. |
multi | [...(sources ?? []), inline?] | Ambient filesystem discovery is never invoked. Skills originate solely from injected sources and inline literals. Explicitly passing sources automatically upgrades tenancy mode to multi. |
Example composing filesystem, organization-wide, and tenant-specific sources:
import { createSeepient, FsSkillSources } from "seepient";
const agent = await createSeepient({
sources: [
new FsSkillSources(process.cwd()), // Local filesystem layers
globalDbSource, // Shared company-wide skills
tenantDbSource, // Tenant-specific overrides (shadows global)
],
});Path reference resolution in injected skills
@path references inside injected/inline skill bodies are NOT resolved on the use_skill tool path (slash-command path only, resolved against process.cwd()); injected skills should avoid @path until 018's invocation rework.
Serverless and bundling disclosures
Serverless filesystem discovery fails silently
In serverless execution environments (AWS Lambda, Vercel Functions, Cloudflare Workers), filesystem-based ambient discovery typically finds no skills directories and produces an empty catalog silently. In serverless deployments, always inject explicit sources or pass inline skill literals (skills: [...]).
Source failure partial fail-open
If an injected skill source's list() throws an error, Seepient logs a warning naming the source label and continues composing the remaining sources rather than halting agent startup (partial fail-open).
Bundling skills in serverless packages
If your serverless deployment relies on filesystem-bundled skills, ensure your framework bundler is configured to trace the skills directory into the output artifact. For example, in Next.js, declare outputFileTracingIncludes in next.config.js:
// next.config.js
module.exports = {
outputFileTracingIncludes: {
'/api/**': ['./skills/**/*'],
},
};Saving generated skills
When Seepient generates a new skill at runtime (e.g. via Spec 016 workflows), or when saving skills programmatically, it saves the skill to the last SkillStore present in the effective sources list:
import {
createSeepient,
saveGeneratedSkill,
type SkillStore,
type SkillRecord,
SkillStoreUnavailableError,
SkillCollisionError,
} from "seepient";
class TenantSkillStore implements SkillStore {
async list(): Promise<SkillRecord[]> {
return [];
}
async save(record: SkillRecord): Promise<void> {
// Persist to tenant database
}
}
const store = new TenantSkillStore();
// Programmatic skill generation save:
const result = await saveGeneratedSkill({
name: "summarize-meeting",
description: "Summarizes meeting notes into action items",
body: "# Summarize Meeting\nFollow these steps to extract action items...",
tags: ["productivity", "meetings"],
sources: [store],
});
console.log(`Saved v${result.version} to store`);
const agent = await createSeepient({
sources: [store],
});Key semantics for saveGeneratedSkill:
- Store destination: Saves to the last
SkillStoreinsources. - Collision refusal: If a skill with the same name exists in any configured source, saving refuses with a
SkillCollisionErrorunlessreplace: trueis set. - Version bump: When
replace: trueis set, the version number automatically increments (e.g.v1 -> v2) and the changelog records the update. - Fails closed: If no
SkillStoreis present insources, skill generation fails closed withSkillStoreUnavailableError(SKILL_STORE_UNAVAILABLE) rather than writing uncontained files to the local disk.
initializeSkillRegistry()
Bootstraps the skill registry by scanning skill directories. This is the public API for initializing skills programmatically:
import { initializeSkillRegistry } from "seepient";
// Scan the current working directory and configured paths for skills
await initializeSkillRegistry(process.cwd());Call this at application startup to ensure skills are discovered before the first agent invocation. Seepient Agent calls this automatically when you pass skills to askSeepient() or createSeepient(), but you may call it explicitly to pre-load skills or inspect the registry.
Skill search paths
Seepient Agent searches for skills in the following locations, in priority order:
| Priority | Path | Source |
|---|---|---|
| 1 | SEEPIENT_SKILLS_PATH env var | Colon-separated custom paths |
| 2 | .seepient/skills/ | Project-level skills |
| 3 | /mnt/skills/ | Docker volume mount |
| 4 | ~/.seepient/skills/ | Global user skills |
| 5 | ~/.agents/skills/ | Cross-agent shared skills |
| 6 | Bundled skills/ directory | Shipped with Seepient Agent |
Higher-priority paths override skills with the same name from lower-priority paths.
Custom skill paths
# Multiple paths, colon-separated
export SEEPIENT_SKILLS_PATH=/opt/skills:/home/user/my-skills# Disable bundled skills
export SEEPIENT_NO_BUNDLED_SKILLS=1Bundled skills
Seepient ships skills in the bundled skills/ directory. They are discovered automatically (lowest priority, so project/user skills of the same name override them).
| Skill | Purpose |
|---|---|
debug-mantra | Four-mantra debugging discipline (reproduce, trace, falsify, cross-reference) |
design | Entry point for all design work via upstream OpenDesign catalog |
docker-ops | Docker container and image operations |
how | Procedural implementation step-by-step guidance |
k8s-deploy | Kubernetes deployment operations |
log-analyzer | Log file analysis and triage |
management-talk | Translate engineering content for leadership and management |
post-mortem | Canonical post-mortem root cause analysis writeup |
repo-review-ultra-deep | Comprehensive, evidence-backed codebase and PR audit |
repo-review-ultra-deep-lite | Token-efficient fast repository and PR review |
scrutinize | Outsider-perspective architectural and change scrutiny |
speckit-* | SpecKit spec-driven workflow (analyze, checklist, clarify, constitution, implement, plan, specify, tasks, taskstoissues) |
unslop | Remove AI tells, filler, and repetitive prose patterns |
why | Architectural rationale and purpose investigation |
Activate any of them by name:
import { askSeepient } from "seepient";
// Route any design request through the OpenDesign catalogue
await askSeepient("Design a dark-mode login screen for a fintech app", {
skills: ["design"],
tools: ["core", "comm", "advanced"],
});INFO
design does not ship design procedures of its own. It fetches the upstream skill over the network on each invocation (sending the request externally), so it needs read_website available and outbound network access.
Skill metadata
Each skill exposes metadata for discovery and filtering:
interface SkillMetadata {
name: string;
description: string;
version: string;
tags: string[];
allowedTools?: string[];
}Creating custom skills
Skills are defined as SKILL.md files inside a named directory. The file uses YAML frontmatter followed by Markdown instructions:
Directory structure
my-skills/
docker-ops/
SKILL.md
code-review/
SKILL.mdSKILL.md format
---
name: docker-ops
description: Docker container and image management operations
version: 1.2.0
author: engineering-team
tags:
- docker
- deployment
- containers
allowedTools:
- execute_shell_command
- read_file
- write_file
priority: 10
args:
- environment
- service
model:
provider: anthropic
model: claude-sonnet-4-6-20260320
---
# Docker Operations Skill
You are a Docker operations specialist. Follow these procedures:
## Building Images
When asked to build a Docker image:
1. Check for an existing Dockerfile using read_file
2. Run: `docker build -t $1-$2 .`
3. Verify the image was created successfully
## Deploying
When deploying to environment $1:
1. Pull the latest image
2. Stop the existing container: `docker stop $2`
3. Start the new container: `docker run -d --name $2 ...`Frontmatter fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique skill identifier (used in skills: [...] option) |
description | string | Yes | Short description shown to the LLM for skill selection |
version | string | No | Semantic version (defaults to "1.0.0") |
author | string | No | Skill author |
tags | string[] | No | Tags for categorization |
allowedTools | string[] | No | Restrict which tools the agent can use when this skill is active |
priority | number | No | Higher priority wins when skills have the same name (default: 0) |
args | string[] | No | Named arguments this skill accepts |
model | object | No | Per-skill model selection (see Provider switching) |
Argument substitution
Skill bodies support template variables that are replaced at invocation time.
Positional arguments
Use $1, $2, ..., $N for positional arguments:
Build and deploy the $2 service to the $1 environment.Invoked as: use_skill({ skill_name: "docker-ops", args: ["staging", "api-gateway"] })
Special variables
| Variable | Description |
|---|---|
$ALL | All arguments joined as a single string |
$COUNT | Number of arguments passed |
$FIRST | First argument (same as $1) |
$LAST | Last argument |
Example with argument substitution
---
name: deploy
description: Deploy a service to an environment
args:
- environment
- service
---
Deploy the $2 service to $1:
1. Build: `docker build -t $2:$1 .`
2. Push: `docker push registry/$2:$1`
3. Deploy: `kubectl apply -f k8s/$1/$2.yaml --namespace $1`The use_skill tool
Skills are exposed to the LLM as the use_skill built-in tool:
// The LLM calls this automatically when a user request matches a skill
{
name: "use_skill",
arguments: {
skill_name: "docker-ops",
args: {
environment: "staging",
service: "api-gateway"
}
}
}You do not call use_skill directly. The LLM decides when to activate a skill based on the user's request and the skill descriptions in its system prompt.
@path file references
Skills support @path references that inline file contents:
Review the code in @src/index.ts and check @package.json for dependencies.Supported patterns:
| Pattern | Resolves to |
|---|---|
@path/to/file | Relative to project root (process.cwd()) |
@seepient_documents/file | ~/seepient_documents/file |
@~/path/to/file | Explicit home directory path |
Files are inlined with syntax highlighting. Limits:
| Limit | Value | Configurable |
|---|---|---|
| References per input | 10 | No |
| Per-file size | 1 MB | No |
| Cumulative total across all inlined files | 2 MB | No |
When the cumulative total exceeds 2 MB, remaining references are skipped with a marker:
[Skipped: cumulative resolved size would exceed 2048KB limit]: @large-file.logWARNING
All resolved paths must fall within the project root, ~/seepient_documents/, or ~/.seepient/. References outside these boundaries are rejected with an access-denied error.
Lazy body loading
Discovery reads only frontmatter (the YAML header) -- the skill body is never parsed at startup. This keeps startup fast regardless of how many skills are installed.
When a skill is invoked, registry.getBody(name) reads the file from disk via skill.filePath, extracts the body text after the --- delimiter, and caches it in an LRU cache with a maximum of 5 entries. When the cache is full, the least-recently-used entry is evicted.
// Internal flow in DefaultSkillRegistry
async getBody(name: string): Promise<string | undefined> {
// 1. Check LRU cache
const cached = this.bodyCache.get(name);
if (cached !== undefined) return cached;
// 2. Read from disk, extract body after frontmatter
const content = await readFile(skill.filePath, 'utf-8');
const body = extractBody(content);
// 3. Cache and evict oldest if over capacity
this.bodyCache.set(name, body);
if (this.bodyCache.size > 5) { /* evict oldest */ }
}The first invocation of any skill incurs a ~1--5 ms disk read, which is negligible compared to LLM API latency. At most 5 skill bodies are held in memory at once, regardless of the total number of installed skills.
Skill catalog injection
After discovery, buildSkillCatalog(metadata) generates a compact text block from SkillMetadata[]. Each skill gets one line in the format - name: description [tags], typically 40--80 characters.
The catalog is composed into the system prompt when constructing the agent context. Across all surfaces (CLI, SDK, Server), it is included in the available skills block of the system context:
AVAILABLE SKILLS (activate with use_skill tool):
- docker-ops: Docker container and image management operations [docker, deployment, containers]
- code-review: Perform code review [review, quality]
When a user request matches a skill, call use_skill with the skill name.Body size limits
Skill bodies are guarded by a three-layer defense against oversized context injection:
Layer 1: Load-time warning (8K chars / ~2K tokens)
When a skill body is read into memory, its length is checked against SEEPIENT_SKILL_BODY_WARN_CHARS (default 8000). If it exceeds the threshold, a warning is emitted:
[SKILLS] Warning: Skill "docker-ops" body is 12450 chars (~3113 tokens).
Consider trimming below 8000 chars for optimal context usage.Layer 2: Injection-time truncation (32K chars / ~8K tokens)
limitSkillBody() enforces a hard cap when the body is about to be injected. If the body exceeds the limit, it is truncated with a clear marker:
[... Skill body truncated: 45000 chars total, 32000 shown.
Reduce skill body size or set SEEPIENT_SKILL_BODY_MAX_CHARS to increase the limit. ...]The function returns a TruncationResult with metadata about original and final sizes:
interface TruncationResult {
body: string; // The (possibly truncated) body
truncated: boolean; // Whether truncation was applied
originalChars: number; // Original size in characters
originalTokenEstimate: number; // Estimated original tokens (chars / 4)
finalChars: number; // Final size in characters
finalTokenEstimate: number; // Estimated final tokens (chars / 4)
}Layer 3: Cumulative @path cap (2 MB total)
In interactive CLI mode, the @path resolver inlines file contents passed as arguments in slash command queries. It stops inlining when cumulative resolved content reaches 2 MB; remaining references are replaced with a skip marker.
Configurable thresholds
| Environment variable | Default | Description |
|---|---|---|
SEEPIENT_SKILL_BODY_MAX_CHARS | 32000 | Hard truncation limit |
SEEPIENT_SKILL_BODY_WARN_CHARS | 8000 | Soft warning threshold |
TIP
If a skill body exceeds the size limit, break it down into modular skills or delegate detailed instructions to specialized sub-skills. Note that @path references are resolved for CLI invocation arguments, not inside skill bodies.
Provider switching
Skills can specify a preferred provider and model in their frontmatter:
---
name: code-review
description: Perform thorough code review
model:
provider: anthropic
model: claude-sonnet-4-6-20260320
---When a skill with a model.provider field is invoked on the interactive slash-command path (REPL, TUI), createRuntimeSkillProviderSwitcher() in src/domain/skills/skill-invoker.ts handles the temporary switch:
- Captures the current provider and model.
- Creates an override for the provider runtime if the skill specifies a different one.
- After the skill execution completes, restores the original provider in a
finallyblock.
// Conceptual: internal domain runtime usage (interactive slash-command path):
// import { createRuntimeSkillProviderSwitcher } from "./domain/skills/skill-invoker.js";
const switcher = createRuntimeSkillProviderSwitcher(runtime);
const switched = await switcher.switchIfNeeded(skillResult);
try {
await agent.chat(skillResult.prompt);
} finally {
if (switched) switcher.restore();
}:::note Scope and limitations
- Interactive slash-command path only: Provider switching is scoped to the interactive slash-command path (REPL, TUI). The LLM-initiated
use_skilltool path executes within the agent's active model without switching providers. - Provider account mapping: The frontmatter
model.providerfield maps directly toproviderAccountinModelAssignmentOverride(skill-invoker.ts:183).assignment-resolver.ts:104looks up the configured account by ID; in single-account setups this matches the vendor name (e.g.anthropic), but in multi-account configurations it requires the exact configured account ID. - Internal API:
createRuntimeSkillProviderSwitcheris an internal domain-layer orchestrator and is not exported from the publicseepientSDK entry. :::
Two invocation paths
Skills can be activated in two ways: via CLI slash commands or via the use_skill tool. Both paths share argument substitution and body size limits, but differ in reference resolution and provider switching: @path resolution and per-skill provider switching apply to the slash-command path only.
Path A: CLI slash command
When the user types a slash-prefixed command in the CLI REPL:
User types: /docker-ops build myapp:latest --no-cacheStep-by-step flow:
- Parse:
parseInvocation()extracts the skill name and arguments.{ skillName: "docker-ops", args: { positional: ["build", "myapp:latest", "--no-cache"], raw: "build myapp:latest --no-cache" } } - Lookup:
registry.get("docker-ops")returns the skill metadata. - Load body:
registry.getBody("docker-ops")reads the file from disk, caches in LRU. - Substitute args:
substituteArgs(body, args)replaces$1,$ALL, etc. - Resolve references:
resolveReferences(body)inlines@pathfiles. - Enforce limits:
limitSkillBody(body)truncates if over 32K chars. - Switch provider:
createRuntimeSkillProviderSwitcher()switches provider if the skill specifiesmodel.provider. - Construct prompt: The body becomes a user message.
[Skill: docker-ops activated] {skill body with substituted args} User request: build myapp:latest --no-cache - Execute:
runAgentLoop()runs with the constructed prompt. - Restore:
switcher.restore()returns the original provider.
Path B: use_skill tool (LLM-initiated)
When the AI decides to use a skill based on the catalog in its system prompt:
AI calls: use_skill({ skill_name: "docker-ops", args: { action: "build", image: "myapp:latest" } })Step-by-step flow:
Tool call received: The
use_skilltool handler receivesskill_nameand optionalargs.Lookup:
registry.get(skill_name)returns the skill metadata.Load body:
registry.getBody(skill_name)reads from disk, caches in LRU.Substitute args: If
argswas provided, values are substituted into the body.Enforce limits:
limitSkillBody(body)truncates if over 32K chars.Return result: Content is returned as a tool result string.
# docker-ops Skill Activated {skill body with substituted args} ## Skill Arguments { "action": "build", "image": "myapp:latest" }The AI receives the content in the next loop iteration and incorporates it into its response.
INFO
The use_skill tool path does not perform provider switching or @path resolution. It returns the skill content as-is for the AI to use. Provider switching only applies to the CLI slash command path where the full invocation pipeline is used.
Related APIs
- askSeepient() -- One-shot execution with skills
- createSeepient() -- Stateful agent with skill support
- Custom Tools -- Build custom tools
- Worker Example -- Full multi-tenant worker with custom skill sources and storage
- Types -- Full TypeScript type reference