- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Portable Agent Skills
withSkills lets the model pick one SKILL.md for the turn. The other skills stay out of the system prompt.
withSkills from @tanstack/ai-skills gives the model a short catalog of your skills and a load_skill tool. The model loads the full instructions only when it needs them.
Note
This is the portable path. It runs on the model you already use and needs no provider sandbox. For skills that run in a provider sandbox, see Provider Skills. You cannot mix the two in one call. See Portable vs hosted.
Quick setup#
1. Install the package#
npm i @tanstack/ai-skillspnpm add @tanstack/ai-skillsyarn add @tanstack/ai-skillsbun add @tanstack/ai-skills2. Add a skill to the server route#
Define a skill inline. Then pass it to withSkills in the middleware array:
// api/chat.ts
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { anthropicText } from '@tanstack/ai-anthropic'
import { inlineSkill, withSkills } from '@tanstack/ai-skills'
const pptx = inlineSkill({
name: 'pptx-builder',
description: 'Build and edit PowerPoint decks with python-pptx.',
instructions: `
# Building a deck
Use python-pptx. Open or create the presentation, edit slides, then save.
Keep one idea per slide.
`,
})
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request)
const stream = chat({
adapter: anthropicText('claude-sonnet-5-5'),
messages: params.messages,
threadId: params.threadId,
middleware: [withSkills(pptx)],
})
return toServerSentEventsResponse(stream)
}Your client does not change. Ask the chat for a slide deck. The model calls load_skill with pptx-builder and then follows the instructions.
How it works#
withSkills adds two things to each request:
- A catalog in the system prompt, with one line per skill (name and description).
- A load_skill tool. Its name input accepts only your skill names, so the model cannot invent one. The tool returns the skill body without its frontmatter, and a list of the bundled resources.
If the model loads the same skill twice in one conversation, the second call returns a short "already loaded" marker, not the full body again.
Offer more than one skill#
Pass an array. withSkills sorts the skills by name and removes duplicates:
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { anthropicText } from '@tanstack/ai-anthropic'
import { inlineSkill, withSkills } from '@tanstack/ai-skills'
const pptx = inlineSkill({
name: 'pptx-builder',
description: 'Build and edit PowerPoint decks with python-pptx.',
instructions: '# Building a deck\nUse python-pptx. Edit slides, then save.',
})
const brand = inlineSkill({
name: 'brand-voice',
description: 'Write in the company brand voice.',
instructions: '# Brand voice\nWarm, direct, no jargon.',
})
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request)
const stream = chat({
adapter: anthropicText('claude-sonnet-5-5'),
messages: params.messages,
threadId: params.threadId,
middleware: [withSkills([pptx, brand])],
})
return toServerSentEventsResponse(stream)
}Most apps keep skills in a folder, a build-time bundle, or a database, not in code. See Skill sources.
Let the model read a skill's files#
Some skills bundle reference files, such as a style guide or a field map. To let the model read them, add createResourceTool to tools. withSkills finds it and tells the model that it can call read_skill_resource:
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { anthropicText } from '@tanstack/ai-anthropic'
import { createResourceTool, inlineSkill, withSkills } from '@tanstack/ai-skills'
const pdf = inlineSkill({
name: 'pdf-filler',
description: 'Fill a PDF form from a data object.',
instructions: '# Fill a PDF\nSee references/fields.md for the field map.',
resources: { 'references/fields.md': 'name -> field_1\nemail -> field_2' },
})
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request)
const stream = chat({
adapter: anthropicText('claude-sonnet-5-5'),
messages: params.messages,
threadId: params.threadId,
tools: [createResourceTool(pdf)],
middleware: [withSkills(pdf)],
})
return toServerSentEventsResponse(stream)
}Without the resource tool, the load_skill result still lists the resources. The model is told that it cannot load them in this setup.
Run a skill's scripts#
Some skills ship scripts, or tell the model to "run python3 extract.py". withSkills lists those scripts in the load_skill result, but it does not run them. Your app runs code through a tool that you supply.
Add an execution tool to chat(). Then write the skill so that it tells the model to call that tool:
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
toolDefinition,
} from '@tanstack/ai'
import { anthropicText } from '@tanstack/ai-anthropic'
import { inlineSkill, withSkills } from '@tanstack/ai-skills'
import { z } from 'zod'
// Your own execution tool. Run the command in a provider sandbox, a local
// isolate, or a serverless worker. Guard it in production.
const executeShell = toolDefinition({
name: 'execute_shell',
description: 'Run a shell command and return its stdout.',
inputSchema: z.object({ command: z.string() }),
outputSchema: z.object({ stdout: z.string() }),
}).server(async ({ command }) => {
const { stdout } = await runInYourSandbox(command)
return { stdout }
})
const extractPdf = inlineSkill({
name: 'pdf-extract',
description: 'Extract text from a PDF with a small Python script.',
instructions: `
# Extract PDF text
Run this with the execute_shell tool, then return the text it prints:
python3 -c "import sys, pypdf; ..."
`,
})
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request)
const stream = chat({
adapter: anthropicText('claude-sonnet-5-5'),
messages: params.messages,
threadId: params.threadId,
tools: [executeShell],
middleware: [withSkills(extractPdf)],
})
return toServerSentEventsResponse(stream)
}You can replace execute_shell with any tool: a container runner, a Code Mode sandbox, or a remote worker. The skill stays the same.
Advanced#
Catalog options#
withSkills(sources, options) accepts these options:
| Option | Default | What it does |
|---|---|---|
| maxCatalogTokens | 4000 | The token limit for the catalog. |
| onLimitExceeded | 'error' | When the catalog is over the limit: 'error' throws. A function (skills, limit) => skills returns a smaller list. |
| requireApproval | false | Asks a human to approve before load_skill or read_skill_resource runs. |
| catalogPlacement | 'system' | Where the catalog goes: 'system' or 'tool-description'. |
| renderCatalog | Per model family | (skills, family) => string. Writes the catalog text yourself. |
| instructionTemplate | Per model family | A template string that must contain {skills}. Escape literal braces as {{ and }}. |
withSkills(sources, {
maxCatalogTokens: 4000,
requireApproval: true,
})Do not pass both renderCatalog and instructionTemplate. If you pass both, withSkills throws.
Catalog format per model family#
By default, Anthropic models get the <available_skills> XML format that they are tuned for. All other models get a plain markdown list.
Next pages#
- Skill sources: load skills from a folder, a build-time bundle, or your own store.
- Write a skill source: back skills with S3, a database, or a registry.