Agent skill bundles
Upload, inspect, and manage per-agent skill bundles from the backend SDKs.
Agent skill bundles
An agent runs with two kinds of skills. System skills are built into the platform and available to every agent — the reusable agentic primitives Wacht maintains, read-only through this API. Agent skills are ones you upload per agent as a ZIP; they land in the agent's filesystem under /skills/agent/<slug> and are tracked in the database so you can list, fetch, and delete them independently.
The backend SDKs (@wacht/backend, wacht-rs) cover the full lifecycle. Every endpoint is on the backend API surface (/ai/agents/{id}/skills/*) — API-key authenticated, not user-session.
Bundle format
A bundle is a ZIP that contains exactly one skill: either a SKILL.md at the zip root, or a single top-level folder containing one. More than one top-level folder is rejected.
---
name: My Skill
description: One-line summary an agent uses to decide when to load this skill.
---
# Body contentThe slug comes from the zip file name (when SKILL.md is at the root) or the single top-level folder name — not from the frontmatter. It's kebab-case normalized. The frontmatter name becomes the display name and description the summary; both are optional, and an upload with no frontmatter still succeeds as long as SKILL.md is present. Bundles are capped at 25 MB; symlinks are rejected.
List the summary
Returns system and agent skills in one call. Each row carries slug, name, optional description, mount_path, and source ("system" or "agent").
Node
import { ai } from "@wacht/backend";
const summary = await ai.listAgentSkillsSummary("agent_id");
console.log("system skills:", summary.system.length);
console.log("agent skills:", summary.agent.length);Rust
let summary = client
.ai()
.agents()
.list_skills_summary("agent_id")
.send()
.await?;Browse the filesystem
list_skill_tree returns the directory listing for a scope (system or agent) at a path. Use it to walk the bundle structure or render a tree UI.
Node
const tree = await ai.listAgentSkillTree("agent_id", "agent", "/");
for (const entry of tree.entries) {
console.log(entry.kind, entry.path);
}Rust
use wacht::models::SkillScope;
let tree = client
.ai()
.agents()
.list_skill_tree("agent_id", SkillScope::Agent)
.send()
.await?;Read a file
Text files come back as content; binaries as content_base64. Check is_text to know which.
Node
const file = await ai.readAgentSkillFile("agent_id", "agent", "/my-skill/SKILL.md");
const body = file.is_text ? file.content : Buffer.from(file.content_base64!, "base64");Rust
let file = client
.ai()
.agents()
.read_skill_file("agent_id", SkillScope::Agent, "/my-skill/SKILL.md")
.send()
.await?;Upload a bundle
Multipart POST. replace_existing decides what happens on a slug collision: true overwrites the matching skill, false errors. The call returns the updated agent skill tree.
Node
const file = new File([buffer], "my-skill.zip", { type: "application/zip" });
const tree = await ai.importAgentSkillBundle("agent_id", file, {
replace_existing: true,
});Rust
let bytes = std::fs::read("./my-skill.zip")?;
let tree = client
.ai()
.agents()
.import_skill_bundle("agent_id", "my-skill.zip", bytes)
.replace_existing(true)
.send()
.await?;Delete an agent skill
Removes the skill row and its files. Only agent-scoped skills are deletable; system skills are read-only through this API and managed at the platform deployment level.
Node
await ai.deleteAgentSkill("agent_id", "my-skill");Rust
client
.ai()
.agents()
.delete_skill("agent_id", "my-skill")
.send()
.await?;