The Problem
Documentation exists, but users still ask the same questions - on Discord, Slack, GitHub issues, and support tickets. The answer is almost always in the docs, but finding it requires keyword searching, reading multiple pages, and piecing together an answer.
What if your docs could just answer questions directly?
How It Works
The Docs Assistant is a template built with 21st SDK and powered by Claude Code. It uses the llms.txt standard - a lightweight, widely-adopted convention for making documentation AI-readable.
Here's the flow:
- On startup - the agent downloads
llms.txtandllms-full.txtfrom your docs site into the sandbox - User asks a question - the agent greps the local docs for relevant keywords
- Focused retrieval - if needed, it fetches specific doc pages for full context
- Streamed answer - Claude responds with citations, linking to the source pages
The whole thing runs in a sandbox. No embeddings, no vector database, no infrastructure to manage.
Agent Tools
The agent has three built-in tools:
| Tool | What it does |
|---|---|
search_docs | Grep through downloaded docs with keyword + surrounding context |
list_doc_pages | Show the full docs index from llms.txt |
fetch_doc_page | Fetch a specific doc page URL for full content |
This keeps the architecture simple: grep first, fetch if needed.
Compatible Sites
Any site that publishes llms.txt works out of the box:
- Anthropic -
https://docs.anthropic.com - Vercel -
https://vercel.com - Supabase -
https://supabase.com/docs - Stripe -
https://docs.stripe.com
If the llms.txt is at a non-standard path, use the DOCS_LLMS_TXT_URL env var to point directly to it.
Getting Started
Clone the template and deploy in under a minute:
git clone https://github.com/21st-dev/21st-sdk-examples.git
cd 21st-sdk-examples/docs-assistant
npm install
npx @21st-sdk/cli login
npx @21st-sdk/cli deploy
Set Your Docs URL
In the 21st dashboard → your agent → Environment Variables:
DOCS_URL=https://docs.anthropic.com
Redeploy after adding the variable:
npx @21st-sdk/cli deploy
Run the Frontend
cp .env.example .env.local
# Add your API_KEY_21ST to .env.local
npm run dev
Open http://localhost:3000 - your docs assistant is live.
Build Your Own
The Docs Assistant is a foundation. Point it at any docs site, embed the chat UI into your product, or extend the agent with more tools. The Support Agent template builds on top of this - adding email escalation via Resend for questions the docs can't answer.
Check the full source on GitHub and deploy your own with 21st SDK.
Agent Template
The full agent source - set DOCS_URL and deploy.
import { agent, tool, Sandbox } from "@21st-sdk/agent"
import { z } from "zod"
export default agent({
runtime: "claude-code",
model: "claude-sonnet-4-6",
permissionMode: "bypassPermissions",
maxTurns: 25,
systemPrompt: `You are a documentation assistant. Answer questions using the
documentation loaded into your workspace.
WORKFLOW:
1. Grep /workspace/llms.txt to find relevant page titles and URLs.
2. Grep /workspace/llms-full.txt for detailed content (if available).
3. If you need more detail, use fetch_doc_page to get the full page content.
4. Synthesize a clear answer with citations.`,
sandbox: Sandbox({
setup: [
`mkdir -p /home/user/workspace && cd /home/user/workspace && \
if [ -n "$DOCS_URL" ]; then \
BASE=$(echo "$DOCS_URL" | sed 's|/$||'); \
curl -fsSL "$BASE/llms.txt" -o llms.txt 2>/dev/null || echo "# Could not fetch llms.txt" > llms.txt; \
curl -fsSL "$BASE/llms-full.txt" -o llms-full.txt 2>/dev/null || true; \
else \
echo "# No DOCS_URL configured. Set DOCS_URL env var." > llms.txt; \
fi`,
],
}),
tools: {
fetch_doc_page: tool({
description: "Fetch a specific documentation page by URL.",
inputSchema: z.object({
url: z.string().url().describe("Full URL of the documentation page"),
}),
execute: async ({ url }) => {
const res = await fetch(url, {
headers: { Accept: "text/plain, text/markdown, text/html" },
signal: AbortSignal.timeout(15_000),
})
const text = await res.text()
return { content: [{ type: "text", text: text.slice(0, 30_000) }] }
},
}),
list_doc_pages: tool({
description: "List all available documentation pages from the llms.txt index.",
inputSchema: z.object({}),
execute: async () => {
const { readFile } = await import("fs/promises")
const content = await readFile("/home/user/workspace/llms.txt", "utf-8")
return { content: [{ type: "text", text: content }] }
},
}),
search_docs: tool({
description: "Search through local documentation files for a keyword.",
inputSchema: z.object({
query: z.string().min(1).describe("Search term to look for in the docs"),
file: z.enum(["llms.txt", "llms-full.txt"]).optional().default("llms-full.txt"),
}),
execute: async ({ query, file }) => {
const { execSync } = await import("child_process")
const result = execSync(
`grep -i -n -C 5 ${JSON.stringify(query)} /home/user/workspace/${file} | head -200`,
{ encoding: "utf-8", timeout: 10_000 },
)
return { content: [{ type: "text", text: result || `No matches found.` }] }
},
}),
},
})



