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.
npm i @tanstack/ai-skillspnpm add @tanstack/ai-skillsyarn add @tanstack/ai-skillsbun add @tanstack/ai-skillsDefine 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.
withSkills adds two things to each request:
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.
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.
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.
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.
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.
By default, Anthropic models get the <available_skills> XML format that they are tuned for. All other models get a plain markdown list.