Agents
Publish a tested agent skill for your SDK
A SKILL.md grounded in your docs that walks coding agents through your SDK's setup, published with your docs site and tested in a fresh project against expected outcomes.
By Hayden Bleasel11 min read

To give coding agents a setup workflow for your SDK, write one SKILL.md that walks through a single task step by step, with every command and code sample copied from your docs and a link to the page it came from. Put it in the folder agents.skills points at, and blume build publishes it with your docs, where developers install it with npx skills add and your docs URL. Then test it the way a developer will meet it: install it in a fresh project, give an agent the task, and check the result against outcomes you wrote down first.
By the end, you have that skill, a trial project that installs it from your docs site, and a script that checks what the agent built. The examples use Acme Messages, the fictional email and SMS API from Generate API docs from an OpenAPI spec, and its TypeScript SDK, @acme/messages. Swap in your own product's names.
You may not need all of this. Every Blume build that knows your site's URL already publishes a skill at /skill.md: a map of your pages, generated without a model. If you only want agents to find the right page, that covers it. And if your skills already live in a GitHub repository, developers can install them from there with npx skills add owner/repo, so publishing them on your docs site is optional.
Pick one task and decide what done looks like
A skill that tries to cover your whole product turns into a worse copy of your docs. Pick the one task developers most often start with or get wrong. For most SDKs, that's setup. For Acme, it's adding the SDK to a Node.js project and sending the welcome email.
Before you write the skill, write down what a correct result looks like, as things a script can check:
@acme/messagesis independencies, notdevDependencies.- The API key comes from
ACME_API_KEY, and no key is written into the code. - The code sends with
acme.messages.sendand thewelcometemplate. - The project type-checks.
Write down the prompt you'll give the agent, too, in a developer's words. Leave out the package, the variable, and the method: supplying those is the skill's job.
Add Acme to this project. Write src/send-welcome.ts, a script that sends our welcome email to the address passed as its first argument.Draft the skill from your docs
Blume ships a skill for this job, blume-write-skill, and the blume skill command opens a coding agent on it. At the root of your docs project, run it without a flag first to see where the skill will go:
npx blume skillIt prints a path like skills/acme/SKILL.md. The name comes from your site's title (Acme becomes acme), which is also the name of the generated skill, so yours takes its place. Then hand the job to an agent:
npx blume skill --claude # or --codexThe agent reads your config, sidebar, and pages, and writes a skill with setup steps, core concepts, common tasks, gotchas, and a link to the page behind each one. If your config doesn't set agents.skills, it adds "./skills". If it doesn't set deployment.site, the agent asks for your site's URL, because every link in the skill is absolute.
The session is interactive, so steer it toward your task: ask it to lead with installing the SDK and sending a first message, and to end the setup with how to verify it. Its closing report lists docs gaps, questions the skill needed answered that no page does. Fix those in the docs as well, since developers hit the same gaps.
Tighten it into a setup workflow
Review the draft like a code change. Every command, config key, and code sample should appear in your docs as written. Then cut anything a capable agent already knows, and anything it can read from a linked page when it needs it. Here's the Acme skill after that pass:
---
name: acme
description: Set up and use Acme Messages, the API and TypeScript SDK (@acme/messages) for sending transactional email and SMS. Use when adding Acme to a project, sending an email or SMS with Acme, checking a message's delivery status, or configuring ACME_API_KEY.
---
# Acme Messages
Acme Messages sends transactional email and SMS from your backend. Use the `@acme/messages` SDK in a Node.js project, or call the REST API at `https://api.acme.example/v1` with a bearer token.
## Set up the SDK
1. Install the SDK as a runtime dependency:
```bash
npm install @acme/messages
```
2. Read the API key from the `ACME_API_KEY` environment variable. Never write a key into code or commit one. If the project has a `.env.example`, add `ACME_API_KEY=` to it.
3. Create one client in a server-side module and reuse it:
```ts
import { Acme } from "@acme/messages";
export const acme = new Acme({ apiKey: process.env.ACME_API_KEY });
```
See [Quickstart](https://docs.acme.example/quickstart.md) and [Authentication](https://docs.acme.example/authentication.md).
## Send a message
```ts
const message = await acme.messages.send({
channel: "email",
to: "ada@example.com",
template: "welcome",
});
console.log(message.id);
```
`channel` is `"email"` or `"sms"`. `template` is the ID of a template in the Acme dashboard. See [Send a message](https://docs.acme.example/messages/send.md) and [Templates](https://docs.acme.example/templates.md).
## Check delivery
```ts
const message = await acme.messages.get("msg_8f2k");
console.log(message.status);
```
See [Message status](https://docs.acme.example/messages/status.md).
## Verify your work
- Run the project's type check, such as `npx tsc --noEmit`.
- Confirm no API key appears in the code or in a committed file.
- Don't send a real message to test the integration unless the user asks. It reaches a real inbox or phone.
## Gotchas
- `send` resolves when Acme accepts the message, not when it's delivered. Read `status` for delivery.
- For `sms`, `to` is an E.164 phone number, like `+15551234567`.
- Keep the client on the server. Code that runs in the browser would expose the key.
## Where to look
- [API reference](https://docs.acme.example/api.md): every endpoint, with a page per operation.
- [Errors](https://docs.acme.example/errors.md): each error code and what to do about it.
- [llms.txt](https://docs.acme.example/llms.txt): every page with a one-line summary.A few choices in it matter more than the rest:
- The description. Agents load a skill when a task matches its
description, so it names the product, the package, the environment variable, and the tasks, in the words developers use. The Agent Skills spec caps it at 1,024 characters. - Setup as numbered steps. The agent follows them in order, and each step is a command or code sample it can copy.
- A verify section. It tells the agent how to check its own work, and it checks the same things as your list of outcomes.
- Links to
.mdURLs. Every Blume page has a Markdown mirror at its URL plus.md, which costs an agent fewer tokens than the HTML. - One file. A skill folder that holds only
SKILL.mdis published as it is and served at/skill.md. Addscripts/orreferences/and Blume publishes the folder as a.tar.gzinstead, which isn't served at/skill.md.
Build it and check the links
blume build publishes skills; blume dev doesn't serve them. These are the parts of the config the skill relies on:
import { defineConfig } from "blume";
export default defineConfig({
// ...the rest of your config
title: "Acme",
deployment: { site: "https://docs.acme.example" },
agents: { skills: "./skills" },
});Build the site and serve it the way a static host would:
npx blume build
npx blume previewThe build log should include these two lines:
Published 1 agent skill(s) (.well-known/agent-skills/index.json)
Generated skill.md (the "acme" skill)One skill, not two: yours replaced the generated one because the names match. In another terminal, fetch the discovery index from the preview server, which listens on http://localhost:4321 unless that port is taken:
curl -s http://localhost:4321/.well-known/agent-skills/index.json{
"$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
"skills": [
{
"description": "Set up and use Acme Messages, the API and TypeScript SDK (@acme/messages) for sending transactional email and SMS. Use when adding Acme to a project, sending an email or SMS with Acme, checking a message's delivery status, or configuring ACME_API_KEY.",
"digest": "sha256:9aea5adea9a81d3f4d12347cdea17fdd49247f7b5ad7d83c6f848e729c2d626b",
"name": "acme",
"type": "skill-md",
"url": "/.well-known/agent-skills/acme/SKILL.md"
}
]
}Your digest will differ. It's the SHA-256 of the published file, and installers check each download against it. Next, request every docs link in the skill from the preview and print any that doesn't return a page:
grep -oE 'https://docs\.acme\.example[^) ]*' skills/acme/SKILL.md | sort -u |
while read -r url; do
code=$(curl -sL -o /dev/null -w "%{http_code}" "http://localhost:4321${url#https://docs.acme.example}")
[ "$code" = "200" ] || echo "$code $url"
doneReplace docs.acme.example with your domain in both places. The loop prints nothing when every link works. Run it on each change: blume validate checks the links in your pages, not the ones in a skill. If you deploy with the vercel() or netlify() adapter on server output, there's no local preview, so run these checks against a preview deployment instead.
Install it in a fresh project
Now meet the skill the way a developer will: from your docs site, in a project that has never seen your SDK. Outside your docs repository, create a folder named acme-trial with these three files:
{
"name": "acme-trial",
"private": true,
"type": "module",
"devDependencies": {
"@types/node": "24.19.0",
"typescript": "7.0.2"
}
}{
"compilerOptions": {
"module": "nodenext",
"target": "es2022",
"strict": true,
"noEmit": true,
"types": ["node"]
},
"include": ["src"]
}The third is the check script, one test per outcome on your list:
// Checks the agent's Acme integration against the expected outcomes.
import { execSync } from "node:child_process";
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
const pkg = JSON.parse(readFileSync("package.json", "utf8"));
const source = existsSync("src")
? readdirSync("src", { recursive: true, withFileTypes: true })
.filter((entry) => entry.isFile() && /\.[cm]?[jt]s$/.test(entry.name))
.map((entry) => readFileSync(join(entry.parentPath, entry.name), "utf8"))
.join("\n")
: "";
const typeChecks = () => {
try {
execSync("npx tsc --noEmit", { stdio: "pipe" });
return true;
} catch {
return false;
}
};
const checks = [
["@acme/messages is a runtime dependency", Boolean(pkg.dependencies?.["@acme/messages"])],
["the key is read from ACME_API_KEY", source.includes("process.env.ACME_API_KEY")],
["no key is written into the code", !/apiKey:\s*["'`]/.test(source)],
["it sends the welcome template", /messages\.send\(/.test(source) && /["'`]welcome["'`]/.test(source)],
["the project type-checks", source !== "" && typeChecks()],
];
for (const [name, passed] of checks) {
console.log(`${passed ? "pass" : "FAIL"} ${name}`);
}
process.exitCode = checks.every(([, passed]) => passed) ? 0 : 1;Install the dependencies, keep an untouched copy to compare against later, and install the skill from the preview:
cd acme-trial
npm install
cp -R ../acme-trial ../acme-baseline
DISABLE_TELEMETRY=1 npx skills@1.7.0 add http://localhost:4321 --skill acme -a claude-code -a codex -yThe skills CLI reads your site's discovery index, checks the file against its digest, and writes it to .agents/skills/acme/SKILL.md, where Codex looks, with a symlink at .claude/skills/acme for Claude Code. It also writes skills-lock.json, which records the source URL and digest. The version is pinned so reruns compare like with like. DISABLE_TELEMETRY=1 turns off the CLI's usage telemetry, which can include the source URL and skill name for sources outside GitHub.
Run the task and check the result
Start claude or codex in the trial folder. Run /skills to confirm acme is listed, then give it the prompt you wrote down. Don't mention the skill: whether the agent picks it up from the description is part of the test. When it's done, run the check:
node check-trial.mjspass @acme/messages is a runtime dependency
pass the key is read from ACME_API_KEY
pass no key is written into the code
pass it sends the welcome template
pass the project type-checksIt exits non-zero on any FAIL, so you can script it later. Then give the same prompt to an agent in acme-baseline, which has no skill, and check that too. The difference between the two runs is what your skill teaches. If the baseline passes as well, either the agent already knows this task or your prompt gives the answer away.
When a check fails with the skill installed, find the step the agent skipped or misread, and rewrite it in the skill. Rebuild, restart the preview, update the installed copy, and run the task again:
DISABLE_TELEMETRY=1 npx skills@1.7.0 update acme -p -yAgents don't give the same answer every time, so a single pass is a sample. Before you trust it, run the trial a few times, each in a fresh copy of acme-baseline with the skill installed.
Publish it and tell developers
Deploy your docs as usual. The skill is then served at /.well-known/agent-skills/acme/SKILL.md and /skill.md, listed in the discovery index and in llms.txt, and, because deployment.site is set, in your site's AI catalog. Run the install once more against production, from the site's root URL:
npx skills add https://docs.acme.example --skill acmeThen put that command where developers start: your quickstart page, your SDK's README, and wherever you tell people to install the package. To change the sample questions the AI catalog lists for the skill, key them by skill: plus its name:
agents: {
skills: "./skills",
catalog: {
queries: {
"skill:acme": ["set up the Acme SDK", "send an email with Acme"],
},
},
},A hand-written skill doesn't update itself. When your SDK or its docs change, run npx blume skill --claude again, which updates the existing file, then rerun the trial and deploy. Developers who installed the skill get the new version with npx skills update acme.
Troubleshooting
skills add finds no skills
The CLI prints No well-known skills found; trying direct download... and then fails with Downloaded URL is not a valid SKILL.md file or supported archive. It found no usable index at that URL. Point it at a build (blume preview or a deployment), not blume dev, and check the build log for a skills warning. A public/.well-known/agent-skills/index.json of your own replaces the generated index. The CLI also drops a skill whose bytes don't match the index's digest, so a CDN serving a stale copy of one of the two files fails the same way: purge both.
The build publishes no skill
The build log names the cause. A warning ending in which does not exist; no skills published means the agents.skills path is wrong; it resolves against the project root. Each skill needs its own folder, like skills/acme/SKILL.md: a SKILL.md directly in skills/ is ignored, and the build warns that there are no publishable skills. A skill with no name or description is skipped with a warning, and so is a name that isn't lowercase letters, digits, and single hyphens, 64 characters at most.
/skill.md still shows the page map
Your skill's name must match the one npx blume skill printed, which comes from the site's title; change the title and the name changes with it. The skill folder must hold only SKILL.md. A public/skill.md of your own wins over both, and agents.skillMd: false turns /skill.md off.
Installing from the /skill.md URL fails
No skills found for the scoped path '/skill.md' means the CLI read the path as a scope and, by design, didn't fall back to the root index. Install from the site's root URL instead, as above.
The agent ignores the skill
If /skills lists it but the agent works from memory, the description doesn't match how the task was phrased. Add the words from your prompt that developers actually use, rebuild, update, and rerun. To test the steps on their own, invoke the skill directly: /acme in Claude Code, $acme in Codex. If /skills doesn't list it and .claude/skills didn't exist when the session started, run /reload-skills in Claude Code, or restart Codex.
Next step
Draft your site's skill
Run it at the root of your docs project, then tighten the draft around the one task you'll test.
npx blume skill --claudeA step here not working for you? Report a broken step.