技能体系 (Skills)
为你的 deep agent 打包专业工作流、领域知识和自定义指南
技能(Skills)遵循开放的 Agent Skills 规范。每个技能都存放在一个包含 SKILL.md 文件的独立目录中,并可选择包含脚本、参考文档和模板等辅助资源。
通过技能,智能体可以具备处理复杂领域任务的标准化操作手册(SOP),而无需在系统提示词中一开始就堆砌所有细则。
基础用法 (Usage)
在创建 deep agent 时,将技能目录路径传递给 skills 参数:
---
name: langgraph-docs
description: Use this skill for requests related to LangGraph in order to fetch relevant documentation to provide accurate, up-to-date guidance.
---
# langgraph-docs
## Overview
This skill explains how to access LangGraph documentation to help answer questions and guide implementation.
## Instructions
### 1. Fetch the documentation index
Use the fetch_url tool to read the following URL:
https://docs.langchain.com/llms.txt
This provides a structured list of all available documentation with descriptions.
### 2. Select relevant documentation
Based on the question, identify 2-4 most relevant documentation URLs from the index. Prioritize:
- Specific how-to guides for implementation questions
- Core concept pages for understanding questions
- Tutorials for end-to-end examples
- Reference docs for API details
### 3. Fetch and synthesize
Use the fetch_url tool to read the selected documentation URLs, then answer the user's question. Give a direct answer first, include the minimum necessary context, and link to the source pages rather than quoting long passages.每个技能目录包含一个定义了元数据和操作步骤的 SKILL.md 文件:
import { createDeepAgent, FilesystemBackend } from "deepagents";
const backend = new FilesystemBackend({ rootDir: process.cwd() });
const agent = await createDeepAgent({
model: "anthropic:claude-sonnet-4-6",
backend,
skills: ["/skills/"],
});工作原理:渐进式披露 (Progressive Disclosure)
如果将几十个技能的完整正文全部硬编码在系统提示词中,会迅速耗尽上下文窗口并引起模型注意力分散。 Deep Agents 采用了优雅的渐进式披露机制:
- 启动阶段:智能体在启动时仅读取所有技能的 Frontmatter 元数据(技能名称
name和简明描述description)。这使得几十个技能在初始状态下仅占用几百个 Token。 - 触发匹配:当用户的任务提示词与某个技能的适用场景匹配时,智能体自主调用内置的
read_file工具去读取该技能目录下的完整SKILL.md。 - 按需深入:如果技能中引用了
scripts/、references/或模板文件,智能体仅在执行到特定子步骤时才读取这些附加资源。
何时使用技能?
- 使用技能:当你有结构化的操作规程、领域规范、代码审查检查单、复杂 API 的最佳实践指南时。
- 使用工具:当你需要执行具有副作用的操作(读写数据库、发起 HTTP 网络请求、执行 Shell 命令)时。
- 使用记忆:当你需要保存跨会话持续更新的用户个人偏好、编码风格或项目上下文时。
编写高效的技能
编写高质量技能的关键在于编写精准的 Frontmatter 描述:
- 良好示例:清晰阐明该技能“做什么”以及“在何时何种条件下触发”。
- 较差示例:描述过于宽泛、模糊,导致模型无法准确建立语义触发关联。
const result = await agent.invoke(
{ messages: [{ role: "user", content: "What is LangGraph?" }] },
{ configurable: { thread_id: "1" } },
);添加辅助资源包 (Supporting resources)
一个完整的技能目录通常组织如下:
scripts/:智能体可以在沙箱中执行的自动化 Python/Node.js 脚本。references/:深入的技术文档、架构图说明或详细的数据字典。assets/:代码模板、配置文件脚手架或样例数据集。
从 SKILL.md 中引用资源
在 SKILL.md 正文中,使用相对路径明确告知智能体何时读取这些支持资源:例如“在执行步骤 3 前,请读取 references/api-spec.json”。
为技能绑定专属工具 (Add tools to skills)
技能可以声明其所依赖的特定工具。智能体只有在激活该技能时,才会动态解析并获得调用这些专属工具的能力。
# Good: specific about what and when
description: >-
Extract text and tables from PDF files, fill PDF forms, and merge
multiple PDFs. Use when working with PDF documents or when the user
mentions PDFs, forms, or document extraction.
# Poor: too vague for reliable matching
description: Helps with PDFs.后端与远程技能加载 (Backends)
技能文件存放在配置好的文件系统后端中。你可以从本地磁盘加载技能,也可以使用 StoreBackend 跨团队共享中央技能仓库。
技能可用性管理 (Skill availability)
按运行时上下文筛选
通过命名空间工厂或中间件,根据当前租户、登录用户角色动态放行对应的技能子集。
隔离技能库
为不同的业务线或助手分配独立的技能目录,防止技能之间产生指令冲突。
动态重新加载技能 (Reload skills)
当底层技能目录中的文件发生变更时,智能体会根据文件修改时间自动感知并在下一次任务中应用最新规范。
固定技能 (Pin skills)
对于核心技能,如果你希望智能体在启动时就直接加载完整内容而无需等待二次触发,可以使用 Pin 机制将其固化。
为子智能体分配技能 (Skills for subagents)
子智能体同样可以拥有专属的技能库。在声明子智能体时为其传入独立的 skills 路径。
技能权限控制 (Skill permissions)
- 只读技能:默认情况下,建议将核心技能目录设置为只读权限,防止智能体在执行任务时意外修改或破坏操作规范。
- 可写技能:在具备自我迭代与元学习能力的智能体中,可允许其根据任务执行经验更新技能库。
排错指南 (Troubleshooting)
- 技能未被激活:检查
description是否足够具体,或尝试使用更明确的意图提示词。 - 辅助文件未找到:确保文件路径相对于技能目录的相对引用正确。
- 脚本执行失败:检查是否配置了具备
execute能力的沙箱后端。
参考规范总结 (Reference)
Frontmatter 核心字段规范
name:技能唯一标识符(英文短横线命名)。description:核心描述,决定了大模型能否精准触发。version:语义化版本号。
For API details, see the [reference guide](references/api-patterns.md).
To extract tables from a PDF, run:
scripts/extract.pyFor each file you reference, state what it contains and when the agent should use it. Keep references one level deep from SKILL.md. Avoid deeply nested reference chains that force the agent through multiple reads to reach the information it needs.
Add tools to skills
A skill can bring its own tools. The agent sees them only after it reads the skill, so their schemas stay out of the prompt until a task needs them.
Skill tools are a Deep Agents feature, not part of the Agent Skills specification. Deep Agents reads the tool names from the spec's free-form metadata field. They are unrelated to the spec's allowed-tools field, which pre-approves tools rather than adding them.
NOTE
Skill tools require deepagents>=1.15.0.
List the tools under metadata.include_tools in the skill's frontmatter, separated by spaces. A YAML list does not match any tool.
name: linear
description: Triage and file Linear issues. Use when the user reports a bug or asks about the Linear backlog.
metadata:
include_tools: list_issues create_issuePassing skills to createDeepAgent creates the skills middleware for you. To add tools, create it yourself with createSkillsMiddleware and pass it in middleware instead. Give it your skill paths as sources, and your tools as tools on the middleware rather than on the agent:
import { tool } from "langchain";
import {
createDeepAgent,
createSkillsMiddleware,
FilesystemBackend,
} from "deepagents";
import { z } from "zod";
const listIssues = tool(
async ({ team }) => `${team}-101: Login page times out`,
{
name: "list_issues",
description: "List open issues for a Linear team.",
schema: z.object({ team: z.string() }),
},
);
const createIssue = tool(async ({ team, title }) => `${team}-102: ${title}`, {
name: "create_issue",
description: "Create a Linear issue and return its ID.",
schema: z.object({ team: z.string(), title: z.string() }),
});
const backend = new FilesystemBackend({
rootDir: "./my-project",
virtualMode: true,
});
const agent = createDeepAgent({
model: "anthropic:claude-sonnet-4-6",
backend,
middleware: [
createSkillsMiddleware({
backend,
sources: ["/skills/"],
tools: [listIssues, createIssue],
}),
],
});Skill tools follow these rules:
- Skill tools are gated. Until the agent reads the skill's
SKILL.md, a call to one fails as an unknown tool, even if the model guesses its name and arguments. - When summarization drops that read from the conversation, the tool is unavailable until the agent reads the skill again.
- Pinning a skill makes its tools available the same way reading its
SKILL.mddoes. They stay available until summarization drops the pinned skill's message.
NOTE
On models that accept new tools mid-conversation, Deep Agents adds a skill tool after the read, which keeps the prompt cache valid. On other models, it adds the tool to the request's tools, which invalidates the cache. See the Anthropic and OpenAI integration pages.
Resolve tools at runtime
The names in include_tools do not have to match a tool's own name. A generated tool name such as mcp_linear_list_issues_ab12 is hard to write by hand, and may not stay the same.
To map the names a skill lists to tools, pass a resolver function as the middleware's tools. The resolver receives each include_tools name and the graph's Runtime, and returns the tools for that name.
The skill lists readable names:
metadata:
include_tools: list_issues create_issueThe resolver maps each one to its real tool:
import type { StructuredTool } from "langchain";
import {
createDeepAgent,
createSkillsMiddleware,
type SkillToolResolver,
} from "deepagents";
// Real names like "mcp_linear_list_issues_ab12"
const toolsByName = new Map<string, StructuredTool>([
["list_issues", listIssues],
["create_issue", createIssue],
]);
const resolveSkillTools: SkillToolResolver = (name) => {
const tool = toolsByName.get(name);
return tool ? [tool] : [];
};
const agent = createDeepAgent({
model: "anthropic:claude-sonnet-4-6",
backend,
middleware: [
createSkillsMiddleware({
backend,
sources: ["/skills/"],
tools: resolveSkillTools,
}),
],
});One name can also stand for many tools, such as every tool from the Linear MCP server. List one name in the skill:
metadata:
include_tools: linearThen return all of the server's tools from the resolver:
// Every tool from the Linear MCP server
const toolsByIntegration = new Map([["linear", linearTools]]);
const resolveSkillTools: SkillToolResolver = (name) =>
toolsByIntegration.get(name) ?? [];The resolver also receives the graph's Runtime, so it can return different tools for each run based on runtime context. For example, look up tools by user:
import {
createDeepAgent,
createSkillsMiddleware,
type SkillToolResolver,
} from "deepagents";
import { z } from "zod";
const contextSchema = z.object({ userId: z.string() });
const resolveSkillTools: SkillToolResolver<z.infer<typeof contextSchema>> = (
name,
runtime,
) => {
if (name !== "linear") return [];
return linearToolsForUser(runtime.context.userId);
};
const agent = createDeepAgent({
model: "anthropic:claude-sonnet-4-6",
backend,
contextSchema,
middleware: [
createSkillsMiddleware({
backend,
sources: ["/skills/"],
tools: resolveSkillTools,
}),
],
});
const result = await agent.invoke(
{
messages: [
{ role: "user", content: "File a bug: checkout button does nothing." },
],
},
{ context: { userId: "user-123" } },
);Open a public LangSmith run for this example.
When you write a resolver:
Keep it fast. It runs on every model call after the agent reads the skill, and again before each skill tool call.
The resolver can be async. It can return a promise of the tools.
Keep a tool searchable
Skill tools, the ones you pass to createSkillsMiddleware, are reachable only through their skill. With providerToolSearchMiddleware, the model finds deferred tools, marked with extras: { defer_loading: true }, by searching for them. To make a tool reachable either way, through search or by reading a skill, pass it to the agent's tools and defer it. Reading the skill then reveals it without a search:
import { providerToolSearchMiddleware, tool } from "langchain";
import { createDeepAgent, createSkillsMiddleware } from "deepagents";
import { z } from "zod";
const createIssue = tool(async ({ team, title }) => `${team}-102: ${title}`, {
name: "create_issue",
description: "Create a Linear issue and return its ID.",
schema: z.object({ team: z.string(), title: z.string() }),
extras: { defer_loading: true },
});
const agent = createDeepAgent({
model: "anthropic:claude-sonnet-4-6",
backend,
tools: [createIssue],
middleware: [
providerToolSearchMiddleware(),
createSkillsMiddleware({ backend, sources: ["/skills/"] }),
],
});Unlike a skill tool, a deferred tool is not gated. The model can find and call it through search before the agent reads the skill.
How the agent reaches a tool depends on where you pass it:
| Pass the tool to | Before the agent reads the skill | After the agent reads the skill |
|---|---|---|
createSkillsMiddleware({ tools }) | Hidden, and calls fail | Visible |
The agent's tools, deferred | Available through provider tool search | Visible without a search |
The agent's tools | Visible, so include_tools has no effect | Visible |
When a skill lists a tool the agent already has, including built-in tools such as edit_file, Deep Agents uses the agent's tool, not one with the same name passed to createSkillsMiddleware.
Backends and remote skill loading
Deep Agents supports different backends depending on how you want to store and manage skill files:
StateBackend: Stores files in LangGraph agent state for the current thread.StoreBackend: Stores files in a LangGraph store for durable, cross-thread storage.FilesystemBackend: Reads and writes skill files from disk under a configurableroot_dir.
StateBackend
import { createDeepAgent, StateBackend, type FileData } from "deepagents";
import { MemorySaver } from "@langchain/langgraph";
const checkpointer = new MemorySaver();
const backend = new StateBackend();
function createFileData(content: string): FileData {
const now = new Date().toISOString();
return {
content: content.split("\n"),
created_at: now,
modified_at: now,
};
}
const skillsFiles: Record<string, FileData> = {};
const skillUrl =
"https://raw.githubusercontent.com/langchain-ai/deepagentsjs/refs/heads/main/examples/skills/langgraph-docs/SKILL.md";
const response = await fetch(skillUrl);
const skillContent = await response.text();
skillsFiles["/skills/langgraph-docs/SKILL.md"] = createFileData(skillContent);
const agent = await createDeepAgent({
model: "anthropic:claude-sonnet-4-6",
backend,
checkpointer, // Required !
// IMPORTANT: deepagents skill source paths are virtual (POSIX) paths relative to the backend root.
skills: ["/skills/"],
});
const config = { configurable: { thread_id: `thread-${Date.now()}` } };
const result = await agent.invoke(
{
messages: [{ role: "user", content: "what is langraph?" }],
files: skillsFiles,
},
config,
);Open a public LangSmith run for this example.
StoreBackend
import { createDeepAgent, StoreBackend } from "deepagents";
import { InMemoryStore, MemorySaver } from "@langchain/langgraph";
const checkpointer = new MemorySaver();
const store = new InMemoryStore();
const backend = new StoreBackend({
namespace: () => ["filesystem"],
store,
});
const skillUrl =
"https://raw.githubusercontent.com/langchain-ai/deepagentsjs/refs/heads/main/examples/skills/langgraph-docs/SKILL.md";
const response = await fetch(skillUrl);
const skillContent = await response.text();
await backend.uploadFiles([
["/skills/langgraph-docs/SKILL.md", new TextEncoder().encode(skillContent)],
]);
const agent = await createDeepAgent({
model: "anthropic:claude-sonnet-4-6",
backend,
store,
checkpointer,
// IMPORTANT: deepagents skill source paths are virtual (POSIX) paths relative to the backend root.
skills: ["/skills/"],
});
const config = {
recursionLimit: 50,
configurable: { thread_id: `thread-${Date.now()}` },
};
const result = await agent.invoke(
{ messages: [{ role: "user", content: "what is langraph?" }] },
config,
);Open a public LangSmith run for this example.
FilesystemBackend
import { createDeepAgent, FilesystemBackend } from "deepagents";
import { MemorySaver } from "@langchain/langgraph";
const checkpointer = new MemorySaver();
const skillUrl =
"https://raw.githubusercontent.com/langchain-ai/deepagentsjs/refs/heads/main/examples/skills/langgraph-docs/SKILL.md";
const response = await fetch(skillUrl);
const skillContent = await response.text();
let backend = new FilesystemBackend({
rootDir: process.cwd(),
virtualMode: true,
});
await backend.uploadFiles([
["/skills/langgraph-docs/SKILL.md", new TextEncoder().encode(skillContent)],
]);
const agent = await createDeepAgent({
model: "anthropic:claude-sonnet-4-6",
backend,
// IMPORTANT: deepagents skill source paths are virtual (POSIX) paths relative to the backend root.
skills: ["/skills/"],
interruptOn: {
read_file: true,
write_file: true,
delete_file: true,
},
checkpointer, // Required for filesystem operations!
});
const config = { configurable: { thread_id: `thread-${Date.now()}` } };
const result = await agent.invoke(
{ messages: [{ role: "user", content: "what is langraph?" }] },
config,
);Open a public LangSmith run for this example.
Skill availability
Not every run needs every skill. Control which skills the agent can see by choosing the paths passed to skills, by resolving those paths against context-specific storage, or by combining multiple skill sources. The context can come from a request handler, auth claims, a graph factory, deployment configuration, or any other application logic.
Select skills by context
Choose the skills array before creating the agent. Use this when skills live in a common location and your application decides which paths apply to the current role, tenant, user, or request type:
import { createDeepAgent } from "deepagents";
const SKILLS_BY_ROLE: Record<string, string[]> = {
engineering: [
"/skills/code-review/",
"/skills/testing/",
"/skills/deployment/",
],
data: [
"/skills/sql-analysis/",
"/skills/visualization/",
"/skills/data-pipeline/",
],
support: ["/skills/ticket-triage/", "/skills/runbook/"],
};
function createAgentForUser(userRole: string) {
return createDeepAgent({
model: "anthropic:claude-sonnet-4-6",
skills: SKILLS_BY_ROLE[userRole] ?? [],
});
}This pattern keeps one maintained copy of each skill and varies only the paths passed to each agent. In deployments, a graph factory is a natural place to parse user information and construct the agent with the right skill paths.
NOTE
The SDK only loads the sources you pass in skills. It does not automatically scan directories such as ~/.deepagents/... or ~/.agents/....
For Deep Agents Code storage conventions, see App data.
Emulate Deep Agents Code source order in the SDK
To match Deep Agents Code layering in SDK code, pass all desired sources explicitly in lowest-to-highest precedence order:
> > [
> > "<user-home>/.deepagents/{agent}/skills/",
> > "<user-home>/.agents/skills/",
> > "<project-root>/.deepagents/skills/",
> > "<project-root>/.agents/skills/",
> > ]
> > ```
> >
> > Then pass that ordered list as `skills` when creating your agent.
### Isolate skill libraries
Use isolated libraries when the same skill path should resolve to different files for different users, roles, tenants, workspaces, or environments. For example, every agent can receive `skills=["/skills/"]`, while the backend resolves `/skills/` against storage scoped to the current identity or deployment context.
The following example uses [StoreBackend](https://reference.langchain.com/javascript/deepagents/backends/StoreBackend), but the same pattern applies to any backend that can resolve files based on runtime context:
```ts theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
import {
createDeepAgent,
CompositeBackend,
StateBackend,
StoreBackend,
} from "deepagents";
const agent = await createDeepAgent({
model: "anthropic:claude-sonnet-4-6",
skills: ["/skills/"],
backend: new CompositeBackend(new StateBackend(), {
"/skills/": new StoreBackend({
namespace: (ctx) => [
ctx.assistantId ?? "default",
ctx.config?.configurable?.user_id ?? "anonymous",
],
}),
}),
});This keeps the agent configuration stable while the backend enforces which library the agent can access. The scope does not need to be user-specific. It can be any boundary your application uses, including role, tenant, workspace, organization, environment, or request type.
To populate a shared library, seed the store from your application code or an admin workflow:
- Namespace by org ID for workspace-wide skills. Pair this with read-only skills when agents should use the library but not modify it.
- Namespace by user ID when each user needs an independent library, as in the example above.
Seed the store with keys like /company-policies/SKILL.md and values that include content and encoding fields. The /skills/ route prefix is stripped before records are read from the store.
You can also combine shared and personal libraries: route /skills/approved/ to an organization-scoped StoreBackend, route /skills/editable/ to a user-scoped backend, and pass both paths in skills. See Writable skills.
Compose multiple sources
Pass multiple skill paths when the agent should see more than one library. For example, you can combine approved organization skills, team-specific workflows, and request-specific instructions:
import { createDeepAgent } from "deepagents";
const agent = await createDeepAgent({
model: "anthropic:claude-sonnet-4-6",
skills: ["/skills/org/", "/skills/team/", "/skills/request/"],
});Each path can point to the same backend, a different backend route, or a backend scoped by runtime context. For a managed solution that handles skill access, sharing, and workspace-level visibility, see Fleet skills.
Reload skills
Skills load once per thread and stay in agent state, so every later model call on that thread reuses the same list. Skills added, edited, or deleted since then never reach the model. This applies when a checkpointer is configured. Without one, state does not survive between runs, so every run loads skills again.
Reset the stored metadata to load every source again on the next model call.
NOTE
Local runs often have no checkpointer, so skill edits take effect immediately and skills appear to reload on their own. LangSmith Deployments configure a persistent checkpointer automatically, so the same agent holds its skills for the life of a thread once deployed. See Going to production.
NOTE
Reloading skills requires deepagents>=1.14.0.
Pass the reset as run input to reload and run in a single call:
const result = await agent.invoke(
{
messages: [{ role: "user", content: "What is LangGraph?" }],
skillsMetadata: null,
},
config,
);Update state to reset a thread between runs:
await agent.updateState(config, { skillsMetadata: null });Use null, not an empty list. An empty list means the sources loaded and held no skills, so the agent sees no skills for the rest of the thread.
NOTE
A reload that finds a different set of skills changes the system prompt and invalidates the prompt cache for that thread. A reload that finds the same skills produces the same prompt and costs nothing.
Middleware can reset the value as well, which lets your application decide when a thread is stale.
Loading happens before each model call, so a reset from a hook takes effect on the next model call, including partway through a run.
import { createMiddleware } from "langchain";
import { StateSchema } from "@langchain/langgraph";
import { skillsMetadataValue } from "deepagents";
const reloadEditedSkills = createMiddleware({
name: "ReloadEditedSkills",
stateSchema: new StateSchema({ skillsMetadata: skillsMetadataValue }),
afterModel: (state) =>
agentEditedSkills(state) ? { skillsMetadata: null } : undefined,
});Pin skills
The agent reads a skill only when it decides the skill matches the task. When a user names a skill in your app, for example by typing /langgraph-docs or choosing it from a menu, pin the skill. Deep Agents then adds the skill's instructions to the conversation, so the agent does not have to choose it.
NOTE
Pinning skills requires deepagents>=1.15.0.
Pass skill names in pinnedSkills with the run input:
const result = await agent.invoke(
{
messages: [{ role: "user", content: "What is LangGraph?" }],
pinnedSkills: ["langgraph-docs"],
},
{ configurable: { thread_id: "1" } },
);Before the next model call, SkillsMiddleware reads each named skill's SKILL.md and appends it to the conversation as a HumanMessage. It adds one message per skill, in the order you name them. Each message holds the file without its frontmatter:
<skill name="langgraph-docs" path="/skills/langgraph-docs/SKILL.md">
# langgraph-docs
## Overview
...
</skill>Each message also sets additional_kwargs.lc_source to "pinned_skill", and puts the skill's name, path, and description in additional_kwargs.skill. A chat UI can use these fields to show a pinned skill as a label instead of its full text.
Pinned skills follow these rules:
- The message stays in the conversation as written. Editing
SKILL.mdlater does not change it, and pinning the skill again appends the current text. Earlier messages never change, so the prompt cache stays valid. - The middleware skips a name that matches no loaded skill, and a skill whose
SKILL.mdit cannot read. - When you pin a skill, its skill tools become available to the agent, the same as when the agent reads its
SKILL.md.
Pin skills named in a message
Deep Agents does not look for skill names in messages. Your app chooses the syntax, finds the names, and passes them in pinnedSkills. The following example pins each skill named as /skill-name anywhere in the message, and sends the text unchanged:
const SKILL_REFERENCE = /(?<!\S)\/([a-z0-9-]+)/g;
const message = "/langgraph-docs How do I add a checkpointer to my graph?";
const pinnedSkills = [...message.matchAll(SKILL_REFERENCE)].map((m) => m[1]); // ["langgraph-docs"]
const result = await agent.invoke(
{
messages: [{ role: "user", content: message }],
pinnedSkills,
},
{ configurable: { thread_id: "1" } },
);The pattern matches a / followed by a skill name, at the start of the message or after a space. It does not check names against your skills, because the middleware skips unknown names. To support another syntax, such as $skill-name, change the pattern.
Skills for subagents
When you use subagents, you can configure which skills each type has access to:
- General-purpose subagent: Automatically inherits skills from the main agent when you pass
skillstocreate_deep_agent. No additional configuration is needed. - Custom subagents: Do not inherit the main agent's skills. Add a
skillsparameter to each subagent definition with that subagent's skill source paths.
Skill state is fully isolated: the main agent's skills are not visible to subagents, and subagent skills are not visible to the main agent.
import { createDeepAgent } from "deepagents";
const researchSubagent = {
name: "researcher",
description: "Research assistant with specialized skills",
systemPrompt: "You are a researcher.",
tools: [webSearch],
skills: ["/skills/research/", "/skills/web-search/"], // Subagent-specific skills
};
const agent = await createDeepAgent({
model: "google:gemini-3.6-flash",
skills: ["/skills/main/"], // Main agent and GP subagent get these
subagents: [researchSubagent], // Researcher gets only its own skills
});For more information on subagent configuration and skills inheritance, see Subagents.
Skill permissions
Skill availability controls which skill files the agent can see. Skill permissions control whether the agent can read those files, write to them, or pause for human approval before writing. Use filesystem permissions for path-based read and write rules, and use interrupt_on or permission rules with mode="interrupt" when writes need approval.
Read-only skills
To let agents use a curated skill library without modifying it, route the skill path to the backend that contains the approved library and deny write operations under that path. The agent can discover and read skills; only your application code or an admin workflow updates the backend.
import { InMemoryStore } from "@langchain/langgraph";
import {
createDeepAgent,
CompositeBackend,
StateBackend,
StoreBackend,
} from "deepagents";
const store = new InMemoryStore(); // Good for local dev; omit for LangSmith Deployment
const agent = createDeepAgent({
model: "google:gemini-3.6-flash",
backend: new CompositeBackend(new StateBackend(), {
"/skills/": new StoreBackend({
namespace: (rt) => ["curated-skills", rt.context.orgId],
}),
}),
skills: ["/skills/"],
permissions: [
{
operations: ["write"],
paths: ["/skills/**"],
mode: "deny",
},
],
store,
});import { InMemoryStore } from "@langchain/langgraph";
import {
createDeepAgent,
CompositeBackend,
StateBackend,
StoreBackend,
} from "deepagents";
const store = new InMemoryStore(); // Good for local dev; omit for LangSmith Deployment
const agent = createDeepAgent({
model: "openai:gpt-5.5",
backend: new CompositeBackend(new StateBackend(), {
"/skills/": new StoreBackend({
namespace: (rt) => ["curated-skills", rt.context.orgId],
}),
}),
skills: ["/skills/"],
permissions: [
{
operations: ["write"],
paths: ["/skills/**"],
mode: "deny",
},
],
store,
});import { InMemoryStore } from "@langchain/langgraph";
import {
createDeepAgent,
CompositeBackend,
StateBackend,
StoreBackend,
} from "deepagents";
const store = new InMemoryStore(); // Good for local dev; omit for LangSmith Deployment
const agent = createDeepAgent({
model: "anthropic:claude-sonnet-5",
backend: new CompositeBackend(new StateBackend(), {
"/skills/": new StoreBackend({
namespace: (rt) => ["curated-skills", rt.context.orgId],
}),
}),
skills: ["/skills/"],
permissions: [
{
operations: ["write"],
paths: ["/skills/**"],
mode: "deny",
},
],
store,
});import { InMemoryStore } from "@langchain/langgraph";
import {
createDeepAgent,
CompositeBackend,
StateBackend,
StoreBackend,
} from "deepagents";
const store = new InMemoryStore(); // Good for local dev; omit for LangSmith Deployment
const agent = createDeepAgent({
model: "openrouter:z-ai/glm-5.2",
backend: new CompositeBackend(new StateBackend(), {
"/skills/": new StoreBackend({
namespace: (rt) => ["curated-skills", rt.context.orgId],
}),
}),
skills: ["/skills/"],
permissions: [
{
operations: ["write"],
paths: ["/skills/**"],
mode: "deny",
},
],
store,
});import { InMemoryStore } from "@langchain/langgraph";
import {
createDeepAgent,
CompositeBackend,
StateBackend,
StoreBackend,
} from "deepagents";
const store = new InMemoryStore(); // Good for local dev; omit for LangSmith Deployment
const agent = createDeepAgent({
model: "fireworks:accounts/fireworks/models/glm-5p2",
backend: new CompositeBackend(new StateBackend(), {
"/skills/": new StoreBackend({
namespace: (rt) => ["curated-skills", rt.context.orgId],
}),
}),
skills: ["/skills/"],
permissions: [
{
operations: ["write"],
paths: ["/skills/**"],
mode: "deny",
},
],
store,
});import { InMemoryStore } from "@langchain/langgraph";
import {
createDeepAgent,
CompositeBackend,
StateBackend,
StoreBackend,
} from "deepagents";
const store = new InMemoryStore(); // Good for local dev; omit for LangSmith Deployment
const agent = createDeepAgent({
model: "baseten:zai-org/GLM-5.2",
backend: new CompositeBackend(new StateBackend(), {
"/skills/": new StoreBackend({
namespace: (rt) => ["curated-skills", rt.context.orgId],
}),
}),
skills: ["/skills/"],
permissions: [
{
operations: ["write"],
paths: ["/skills/**"],
mode: "deny",
},
],
store,
});import { InMemoryStore } from "@langchain/langgraph";
import {
createDeepAgent,
CompositeBackend,
StateBackend,
StoreBackend,
} from "deepagents";
const store = new InMemoryStore(); // Good for local dev; omit for LangSmith Deployment
const agent = createDeepAgent({
model: "ollama:north-mini-code-1.0",
backend: new CompositeBackend(new StateBackend(), {
"/skills/": new StoreBackend({
namespace: (rt) => ["curated-skills", rt.context.orgId],
}),
}),
skills: ["/skills/"],
permissions: [
{
operations: ["write"],
paths: ["/skills/**"],
mode: "deny",
},
],
store,
});Use this for enterprise knowledge bases, approved tool instructions, or centrally managed skill packs where the agent should use the content but should not rewrite the source of truth.
Writable skills
By default, agents can write to skill files if the backend permits it and no permission rule blocks the path. To let agents create or refine some skills while protecting others:
- Route writable paths to a backend that can persist agent edits.
- Pass those paths in
skills. - Add
denyrules for any paths that should stay read-only. Place more specific rules before broader deny rules if paths overlap (rule ordering).
import { InMemoryStore } from "@langchain/langgraph";
import {
createDeepAgent,
CompositeBackend,
StateBackend,
StoreBackend,
} from "deepagents";
const store = new InMemoryStore(); // Good for local dev; omit for LangSmith Deployment
const agent = await createDeepAgent({
model: "anthropic:claude-sonnet-4-6",
backend: new CompositeBackend(new StateBackend(), {
"/skills/approved/": new StoreBackend({
namespace: (rt) => ["approved-skills", rt.context.orgId],
}),
"/skills/editable/": new StoreBackend({
namespace: (ctx) => [
"editable-skills",
ctx.config?.configurable?.user_id ?? "anonymous",
],
}),
}),
skills: ["/skills/approved/", "/skills/editable/"],
permissions: [
{
operations: ["write"],
paths: ["/skills/approved/**"],
mode: "deny",
},
],
store,
});The agent uses write_file and edit_file to create or update SKILL.md and supporting files under writable paths. To capture general learnings outside the skills format, route a separate path such as /memories/ to another writable backend. See Backends for routing and store setup.
Write approval
If agents may write to skill files but you want a human in the loop first, use either interrupt_on or a permission rule with mode="interrupt". Both pause before write_file or edit_file runs and use the same resume flow.
import { MemorySaver } from "@langchain/langgraph";
import { createDeepAgent } from "deepagents";
const agent = await createDeepAgent({
model: "anthropic:claude-sonnet-4-6",
skills: ["/skills/editable/"],
permissions: [
{
operations: ["write"],
paths: ["/skills/**"],
mode: "interrupt",
},
],
checkpointer: new MemorySaver(), // Required to pause and resume
});Alternatively, configure interrupt_on={"write_file": True, "edit_file": True} to require approval for all filesystem writes, not only skills paths. See Human-in-the-loop for handling and resuming interrupts.
Execute code with skills
Without code execution, skills are passive: the agent reads instructions and follows them using its available tools. Code execution turns skills into active capabilities. A skill can ship a tested script that calls an API, transforms data, validates output, or runs a pipeline — and the agent executes it deterministically rather than regenerating the logic from instructions each time. This is especially valuable for workflows that require exact behavior (data transformations, API integrations, compliance checks) or that depend on libraries the agent cannot use through tool calls alone.
Skills execute code through sandbox scripts: the agent runs a bundled script when it needs to install dependencies, run tests, call CLIs, or work with an operating-system filesystem.
Sandbox scripts
Skills can include scripts alongside the SKILL.md file. Reference scripts in your SKILL.md so the agent knows they exist and when to run them:
skills
arxiv-search
SKILL.md
scripts
search.ts
---
name: arxiv-search
description: Search the arXiv preprint repository for research papers. Use when the user asks about academic papers, recent research, or scientific literature.
---
# arxiv-search
Search arXiv for papers matching the user's query.
## Instructions
1. Run `scripts/search.ts` with the user's query as an argument.
2. Parse the results and present them with title, authors, abstract summary, and link.
3. If the user asks for more detail on a specific paper, fetch the full abstract.The agent can read scripts from any backend, but to execute them, the agent needs access to a shell, which only sandbox backends provide.
Sandbox backends run in isolated containers. Skill files stored outside the sandbox are not available inside it, which means the agent cannot execute skill scripts or access skill resources unless they are transferred in first. Use custom middleware to handle this transfer:
before_agent: Read skill files from the backend and upload them into the sandbox so the agent can execute scripts from the start.after_agent: Download any updated or newly created skill files from the sandbox and write them back to the backend so changes persist across runs.
import { readFile, readdir } from "node:fs/promises";
import { join, posix, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createMiddleware } from "langchain";
import {
CompositeBackend,
createDeepAgent,
type FileData,
LangSmithSandbox,
StoreBackend,
} from "deepagents";
import { InMemoryStore } from "@langchain/langgraph";
import { SandboxClient } from "langsmith/sandbox";
/** Identical skill bundles for every user: one shared store namespace. */
const SKILLS_SHARED_NAMESPACE = ["skills", "builtin"] as const;
function createFileData(content: string): FileData {
const now = new Date().toISOString();
return {
content: content.split("\n"),
created_at: now,
modified_at: now,
};
}
function normalizeSkillsStoreKey(key: string): string {
const k = String(key);
if (k.includes("..") || /[*?]/.test(k)) {
throw new Error(`Invalid key: ${key}`);
}
return k.startsWith("/") ? k : `/${k}`;
}
async function walkFiles(dir: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await walkFiles(fullPath)));
} else if (entry.isFile()) {
files.push(fullPath);
}
}
return files.sort((a, b) => a.localeCompare(b));
}
/** Load canonical skill files from disk into the shared store namespace (run once at deploy).
* You can retrieve skills from any source (local filesystem, remote URL, etc.).
*/
async function seedSkillStore(store: InMemoryStore) {
const moduleDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
const skillsDir = resolve(moduleDir, "skills");
const filePaths = await walkFiles(skillsDir);
for (const filePath of filePaths) {
const rel = relative(skillsDir, filePath);
// StoreBackend keys are paths *relative to the routed backend root*.
// CompositeBackend strips the route prefix (`/skills/`) before delegating,
// so store keys should look like "/<skillname>/SKILL.md".
const key = `/${posix.normalize(rel.split("\\").join("/"))}`;
const content = await readFile(filePath, "utf8");
await store.put([...SKILLS_SHARED_NAMESPACE], key, createFileData(content));
}
}
/** Copy shared skill files from the store into the sandbox before each agent run. */
function createSkillSandboxSyncMiddleware(backend: CompositeBackend) {
return createMiddleware({
name: "SkillSandboxSyncMiddleware",
beforeAgent: async (state, runtime) => {
const store = (runtime as any).store;
if (!store) {
throw new Error(
"Store is required for syncing skills into the sandbox. " +
"Pass `store` to createDeepAgent and ensure your runtime provides it.",
);
}
const encoder = new TextEncoder();
const files: Array<[string, Uint8Array]> = [];
for (const item of await store.search([...SKILLS_SHARED_NAMESPACE])) {
const normalized = normalizeSkillsStoreKey(String(item.key));
const data = item.value as FileData;
// CompositeBackend routes paths and batches uploads to the right backend.
files.push([
`/skills${normalized}`,
encoder.encode(data.content.join("\n")),
]);
}
if (files.length > 0) await backend.uploadFiles(files);
return state;
},
});
}
async function main() {
const store = new InMemoryStore();
await seedSkillStore(store);
const client = new SandboxClient();
const lsSandbox = await client.createSandbox();
const backend = new CompositeBackend(
new LangSmithSandbox({ sandbox: lsSandbox }),
{
"/skills/": new StoreBackend({
store,
namespace: () => [...SKILLS_SHARED_NAMESPACE],
} as any),
},
);
try {
const agent = await createDeepAgent({
model: "google:gemini-3.6-flash",
backend,
skills: ["/skills/"],
store,
middleware: [createSkillSandboxSyncMiddleware(backend)],
});
} finally {
await client.deleteSandbox(lsSandbox.name);
}
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});import { readFile, readdir } from "node:fs/promises";
import { join, posix, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createMiddleware } from "langchain";
import {
CompositeBackend,
createDeepAgent,
type FileData,
LangSmithSandbox,
StoreBackend,
} from "deepagents";
import { InMemoryStore } from "@langchain/langgraph";
import { SandboxClient } from "langsmith/sandbox";
/** Identical skill bundles for every user: one shared store namespace. */
const SKILLS_SHARED_NAMESPACE = ["skills", "builtin"] as const;
function createFileData(content: string): FileData {
const now = new Date().toISOString();
return {
content: content.split("\n"),
created_at: now,
modified_at: now,
};
}
function normalizeSkillsStoreKey(key: string): string {
const k = String(key);
if (k.includes("..") || /[*?]/.test(k)) {
throw new Error(`Invalid key: ${key}`);
}
return k.startsWith("/") ? k : `/${k}`;
}
async function walkFiles(dir: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await walkFiles(fullPath)));
} else if (entry.isFile()) {
files.push(fullPath);
}
}
return files.sort((a, b) => a.localeCompare(b));
}
/** Load canonical skill files from disk into the shared store namespace (run once at deploy).
* You can retrieve skills from any source (local filesystem, remote URL, etc.).
*/
async function seedSkillStore(store: InMemoryStore) {
const moduleDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
const skillsDir = resolve(moduleDir, "skills");
const filePaths = await walkFiles(skillsDir);
for (const filePath of filePaths) {
const rel = relative(skillsDir, filePath);
// StoreBackend keys are paths *relative to the routed backend root*.
// CompositeBackend strips the route prefix (`/skills/`) before delegating,
// so store keys should look like "/<skillname>/SKILL.md".
const key = `/${posix.normalize(rel.split("\\").join("/"))}`;
const content = await readFile(filePath, "utf8");
await store.put([...SKILLS_SHARED_NAMESPACE], key, createFileData(content));
}
}
/** Copy shared skill files from the store into the sandbox before each agent run. */
function createSkillSandboxSyncMiddleware(backend: CompositeBackend) {
return createMiddleware({
name: "SkillSandboxSyncMiddleware",
beforeAgent: async (state, runtime) => {
const store = (runtime as any).store;
if (!store) {
throw new Error(
"Store is required for syncing skills into the sandbox. " +
"Pass `store` to createDeepAgent and ensure your runtime provides it.",
);
}
const encoder = new TextEncoder();
const files: Array<[string, Uint8Array]> = [];
for (const item of await store.search([...SKILLS_SHARED_NAMESPACE])) {
const normalized = normalizeSkillsStoreKey(String(item.key));
const data = item.value as FileData;
// CompositeBackend routes paths and batches uploads to the right backend.
files.push([
`/skills${normalized}`,
encoder.encode(data.content.join("\n")),
]);
}
if (files.length > 0) await backend.uploadFiles(files);
return state;
},
});
}
async function main() {
const store = new InMemoryStore();
await seedSkillStore(store);
const client = new SandboxClient();
const lsSandbox = await client.createSandbox();
const backend = new CompositeBackend(
new LangSmithSandbox({ sandbox: lsSandbox }),
{
"/skills/": new StoreBackend({
store,
namespace: () => [...SKILLS_SHARED_NAMESPACE],
} as any),
},
);
try {
const agent = await createDeepAgent({
model: "openai:gpt-5.5",
backend,
skills: ["/skills/"],
store,
middleware: [createSkillSandboxSyncMiddleware(backend)],
});
} finally {
await client.deleteSandbox(lsSandbox.name);
}
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});import { readFile, readdir } from "node:fs/promises";
import { join, posix, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createMiddleware } from "langchain";
import {
CompositeBackend,
createDeepAgent,
type FileData,
LangSmithSandbox,
StoreBackend,
} from "deepagents";
import { InMemoryStore } from "@langchain/langgraph";
import { SandboxClient } from "langsmith/sandbox";
/** Identical skill bundles for every user: one shared store namespace. */
const SKILLS_SHARED_NAMESPACE = ["skills", "builtin"] as const;
function createFileData(content: string): FileData {
const now = new Date().toISOString();
return {
content: content.split("\n"),
created_at: now,
modified_at: now,
};
}
function normalizeSkillsStoreKey(key: string): string {
const k = String(key);
if (k.includes("..") || /[*?]/.test(k)) {
throw new Error(`Invalid key: ${key}`);
}
return k.startsWith("/") ? k : `/${k}`;
}
async function walkFiles(dir: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await walkFiles(fullPath)));
} else if (entry.isFile()) {
files.push(fullPath);
}
}
return files.sort((a, b) => a.localeCompare(b));
}
/** Load canonical skill files from disk into the shared store namespace (run once at deploy).
* You can retrieve skills from any source (local filesystem, remote URL, etc.).
*/
async function seedSkillStore(store: InMemoryStore) {
const moduleDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
const skillsDir = resolve(moduleDir, "skills");
const filePaths = await walkFiles(skillsDir);
for (const filePath of filePaths) {
const rel = relative(skillsDir, filePath);
// StoreBackend keys are paths *relative to the routed backend root*.
// CompositeBackend strips the route prefix (`/skills/`) before delegating,
// so store keys should look like "/<skillname>/SKILL.md".
const key = `/${posix.normalize(rel.split("\\").join("/"))}`;
const content = await readFile(filePath, "utf8");
await store.put([...SKILLS_SHARED_NAMESPACE], key, createFileData(content));
}
}
/** Copy shared skill files from the store into the sandbox before each agent run. */
function createSkillSandboxSyncMiddleware(backend: CompositeBackend) {
return createMiddleware({
name: "SkillSandboxSyncMiddleware",
beforeAgent: async (state, runtime) => {
const store = (runtime as any).store;
if (!store) {
throw new Error(
"Store is required for syncing skills into the sandbox. " +
"Pass `store` to createDeepAgent and ensure your runtime provides it.",
);
}
const encoder = new TextEncoder();
const files: Array<[string, Uint8Array]> = [];
for (const item of await store.search([...SKILLS_SHARED_NAMESPACE])) {
const normalized = normalizeSkillsStoreKey(String(item.key));
const data = item.value as FileData;
// CompositeBackend routes paths and batches uploads to the right backend.
files.push([
`/skills${normalized}`,
encoder.encode(data.content.join("\n")),
]);
}
if (files.length > 0) await backend.uploadFiles(files);
return state;
},
});
}
async function main() {
const store = new InMemoryStore();
await seedSkillStore(store);
const client = new SandboxClient();
const lsSandbox = await client.createSandbox();
const backend = new CompositeBackend(
new LangSmithSandbox({ sandbox: lsSandbox }),
{
"/skills/": new StoreBackend({
store,
namespace: () => [...SKILLS_SHARED_NAMESPACE],
} as any),
},
);
try {
const agent = await createDeepAgent({
model: "anthropic:claude-sonnet-5",
backend,
skills: ["/skills/"],
store,
middleware: [createSkillSandboxSyncMiddleware(backend)],
});
} finally {
await client.deleteSandbox(lsSandbox.name);
}
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});import { readFile, readdir } from "node:fs/promises";
import { join, posix, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createMiddleware } from "langchain";
import {
CompositeBackend,
createDeepAgent,
type FileData,
LangSmithSandbox,
StoreBackend,
} from "deepagents";
import { InMemoryStore } from "@langchain/langgraph";
import { SandboxClient } from "langsmith/sandbox";
/** Identical skill bundles for every user: one shared store namespace. */
const SKILLS_SHARED_NAMESPACE = ["skills", "builtin"] as const;
function createFileData(content: string): FileData {
const now = new Date().toISOString();
return {
content: content.split("\n"),
created_at: now,
modified_at: now,
};
}
function normalizeSkillsStoreKey(key: string): string {
const k = String(key);
if (k.includes("..") || /[*?]/.test(k)) {
throw new Error(`Invalid key: ${key}`);
}
return k.startsWith("/") ? k : `/${k}`;
}
async function walkFiles(dir: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await walkFiles(fullPath)));
} else if (entry.isFile()) {
files.push(fullPath);
}
}
return files.sort((a, b) => a.localeCompare(b));
}
/** Load canonical skill files from disk into the shared store namespace (run once at deploy).
* You can retrieve skills from any source (local filesystem, remote URL, etc.).
*/
async function seedSkillStore(store: InMemoryStore) {
const moduleDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
const skillsDir = resolve(moduleDir, "skills");
const filePaths = await walkFiles(skillsDir);
for (const filePath of filePaths) {
const rel = relative(skillsDir, filePath);
// StoreBackend keys are paths *relative to the routed backend root*.
// CompositeBackend strips the route prefix (`/skills/`) before delegating,
// so store keys should look like "/<skillname>/SKILL.md".
const key = `/${posix.normalize(rel.split("\\").join("/"))}`;
const content = await readFile(filePath, "utf8");
await store.put([...SKILLS_SHARED_NAMESPACE], key, createFileData(content));
}
}
/** Copy shared skill files from the store into the sandbox before each agent run. */
function createSkillSandboxSyncMiddleware(backend: CompositeBackend) {
return createMiddleware({
name: "SkillSandboxSyncMiddleware",
beforeAgent: async (state, runtime) => {
const store = (runtime as any).store;
if (!store) {
throw new Error(
"Store is required for syncing skills into the sandbox. " +
"Pass `store` to createDeepAgent and ensure your runtime provides it.",
);
}
const encoder = new TextEncoder();
const files: Array<[string, Uint8Array]> = [];
for (const item of await store.search([...SKILLS_SHARED_NAMESPACE])) {
const normalized = normalizeSkillsStoreKey(String(item.key));
const data = item.value as FileData;
// CompositeBackend routes paths and batches uploads to the right backend.
files.push([
`/skills${normalized}`,
encoder.encode(data.content.join("\n")),
]);
}
if (files.length > 0) await backend.uploadFiles(files);
return state;
},
});
}
async function main() {
const store = new InMemoryStore();
await seedSkillStore(store);
const client = new SandboxClient();
const lsSandbox = await client.createSandbox();
const backend = new CompositeBackend(
new LangSmithSandbox({ sandbox: lsSandbox }),
{
"/skills/": new StoreBackend({
store,
namespace: () => [...SKILLS_SHARED_NAMESPACE],
} as any),
},
);
try {
const agent = await createDeepAgent({
model: "openrouter:z-ai/glm-5.2",
backend,
skills: ["/skills/"],
store,
middleware: [createSkillSandboxSyncMiddleware(backend)],
});
} finally {
await client.deleteSandbox(lsSandbox.name);
}
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});import { readFile, readdir } from "node:fs/promises";
import { join, posix, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createMiddleware } from "langchain";
import {
CompositeBackend,
createDeepAgent,
type FileData,
LangSmithSandbox,
StoreBackend,
} from "deepagents";
import { InMemoryStore } from "@langchain/langgraph";
import { SandboxClient } from "langsmith/sandbox";
/** Identical skill bundles for every user: one shared store namespace. */
const SKILLS_SHARED_NAMESPACE = ["skills", "builtin"] as const;
function createFileData(content: string): FileData {
const now = new Date().toISOString();
return {
content: content.split("\n"),
created_at: now,
modified_at: now,
};
}
function normalizeSkillsStoreKey(key: string): string {
const k = String(key);
if (k.includes("..") || /[*?]/.test(k)) {
throw new Error(`Invalid key: ${key}`);
}
return k.startsWith("/") ? k : `/${k}`;
}
async function walkFiles(dir: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await walkFiles(fullPath)));
} else if (entry.isFile()) {
files.push(fullPath);
}
}
return files.sort((a, b) => a.localeCompare(b));
}
/** Load canonical skill files from disk into the shared store namespace (run once at deploy).
* You can retrieve skills from any source (local filesystem, remote URL, etc.).
*/
async function seedSkillStore(store: InMemoryStore) {
const moduleDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
const skillsDir = resolve(moduleDir, "skills");
const filePaths = await walkFiles(skillsDir);
for (const filePath of filePaths) {
const rel = relative(skillsDir, filePath);
// StoreBackend keys are paths *relative to the routed backend root*.
// CompositeBackend strips the route prefix (`/skills/`) before delegating,
// so store keys should look like "/<skillname>/SKILL.md".
const key = `/${posix.normalize(rel.split("\\").join("/"))}`;
const content = await readFile(filePath, "utf8");
await store.put([...SKILLS_SHARED_NAMESPACE], key, createFileData(content));
}
}
/** Copy shared skill files from the store into the sandbox before each agent run. */
function createSkillSandboxSyncMiddleware(backend: CompositeBackend) {
return createMiddleware({
name: "SkillSandboxSyncMiddleware",
beforeAgent: async (state, runtime) => {
const store = (runtime as any).store;
if (!store) {
throw new Error(
"Store is required for syncing skills into the sandbox. " +
"Pass `store` to createDeepAgent and ensure your runtime provides it.",
);
}
const encoder = new TextEncoder();
const files: Array<[string, Uint8Array]> = [];
for (const item of await store.search([...SKILLS_SHARED_NAMESPACE])) {
const normalized = normalizeSkillsStoreKey(String(item.key));
const data = item.value as FileData;
// CompositeBackend routes paths and batches uploads to the right backend.
files.push([
`/skills${normalized}`,
encoder.encode(data.content.join("\n")),
]);
}
if (files.length > 0) await backend.uploadFiles(files);
return state;
},
});
}
async function main() {
const store = new InMemoryStore();
await seedSkillStore(store);
const client = new SandboxClient();
const lsSandbox = await client.createSandbox();
const backend = new CompositeBackend(
new LangSmithSandbox({ sandbox: lsSandbox }),
{
"/skills/": new StoreBackend({
store,
namespace: () => [...SKILLS_SHARED_NAMESPACE],
} as any),
},
);
try {
const agent = await createDeepAgent({
model: "fireworks:accounts/fireworks/models/glm-5p2",
backend,
skills: ["/skills/"],
store,
middleware: [createSkillSandboxSyncMiddleware(backend)],
});
} finally {
await client.deleteSandbox(lsSandbox.name);
}
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});import { readFile, readdir } from "node:fs/promises";
import { join, posix, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createMiddleware } from "langchain";
import {
CompositeBackend,
createDeepAgent,
type FileData,
LangSmithSandbox,
StoreBackend,
} from "deepagents";
import { InMemoryStore } from "@langchain/langgraph";
import { SandboxClient } from "langsmith/sandbox";
/** Identical skill bundles for every user: one shared store namespace. */
const SKILLS_SHARED_NAMESPACE = ["skills", "builtin"] as const;
function createFileData(content: string): FileData {
const now = new Date().toISOString();
return {
content: content.split("\n"),
created_at: now,
modified_at: now,
};
}
function normalizeSkillsStoreKey(key: string): string {
const k = String(key);
if (k.includes("..") || /[*?]/.test(k)) {
throw new Error(`Invalid key: ${key}`);
}
return k.startsWith("/") ? k : `/${k}`;
}
async function walkFiles(dir: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await walkFiles(fullPath)));
} else if (entry.isFile()) {
files.push(fullPath);
}
}
return files.sort((a, b) => a.localeCompare(b));
}
/** Load canonical skill files from disk into the shared store namespace (run once at deploy).
* You can retrieve skills from any source (local filesystem, remote URL, etc.).
*/
async function seedSkillStore(store: InMemoryStore) {
const moduleDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
const skillsDir = resolve(moduleDir, "skills");
const filePaths = await walkFiles(skillsDir);
for (const filePath of filePaths) {
const rel = relative(skillsDir, filePath);
// StoreBackend keys are paths *relative to the routed backend root*.
// CompositeBackend strips the route prefix (`/skills/`) before delegating,
// so store keys should look like "/<skillname>/SKILL.md".
const key = `/${posix.normalize(rel.split("\\").join("/"))}`;
const content = await readFile(filePath, "utf8");
await store.put([...SKILLS_SHARED_NAMESPACE], key, createFileData(content));
}
}
/** Copy shared skill files from the store into the sandbox before each agent run. */
function createSkillSandboxSyncMiddleware(backend: CompositeBackend) {
return createMiddleware({
name: "SkillSandboxSyncMiddleware",
beforeAgent: async (state, runtime) => {
const store = (runtime as any).store;
if (!store) {
throw new Error(
"Store is required for syncing skills into the sandbox. " +
"Pass `store` to createDeepAgent and ensure your runtime provides it.",
);
}
const encoder = new TextEncoder();
const files: Array<[string, Uint8Array]> = [];
for (const item of await store.search([...SKILLS_SHARED_NAMESPACE])) {
const normalized = normalizeSkillsStoreKey(String(item.key));
const data = item.value as FileData;
// CompositeBackend routes paths and batches uploads to the right backend.
files.push([
`/skills${normalized}`,
encoder.encode(data.content.join("\n")),
]);
}
if (files.length > 0) await backend.uploadFiles(files);
return state;
},
});
}
async function main() {
const store = new InMemoryStore();
await seedSkillStore(store);
const client = new SandboxClient();
const lsSandbox = await client.createSandbox();
const backend = new CompositeBackend(
new LangSmithSandbox({ sandbox: lsSandbox }),
{
"/skills/": new StoreBackend({
store,
namespace: () => [...SKILLS_SHARED_NAMESPACE],
} as any),
},
);
try {
const agent = await createDeepAgent({
model: "baseten:zai-org/GLM-5.2",
backend,
skills: ["/skills/"],
store,
middleware: [createSkillSandboxSyncMiddleware(backend)],
});
} finally {
await client.deleteSandbox(lsSandbox.name);
}
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});import { readFile, readdir } from "node:fs/promises";
import { join, posix, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createMiddleware } from "langchain";
import {
CompositeBackend,
createDeepAgent,
type FileData,
LangSmithSandbox,
StoreBackend,
} from "deepagents";
import { InMemoryStore } from "@langchain/langgraph";
import { SandboxClient } from "langsmith/sandbox";
/** Identical skill bundles for every user: one shared store namespace. */
const SKILLS_SHARED_NAMESPACE = ["skills", "builtin"] as const;
function createFileData(content: string): FileData {
const now = new Date().toISOString();
return {
content: content.split("\n"),
created_at: now,
modified_at: now,
};
}
function normalizeSkillsStoreKey(key: string): string {
const k = String(key);
if (k.includes("..") || /[*?]/.test(k)) {
throw new Error(`Invalid key: ${key}`);
}
return k.startsWith("/") ? k : `/${k}`;
}
async function walkFiles(dir: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await walkFiles(fullPath)));
} else if (entry.isFile()) {
files.push(fullPath);
}
}
return files.sort((a, b) => a.localeCompare(b));
}
/** Load canonical skill files from disk into the shared store namespace (run once at deploy).
* You can retrieve skills from any source (local filesystem, remote URL, etc.).
*/
async function seedSkillStore(store: InMemoryStore) {
const moduleDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
const skillsDir = resolve(moduleDir, "skills");
const filePaths = await walkFiles(skillsDir);
for (const filePath of filePaths) {
const rel = relative(skillsDir, filePath);
// StoreBackend keys are paths *relative to the routed backend root*.
// CompositeBackend strips the route prefix (`/skills/`) before delegating,
// so store keys should look like "/<skillname>/SKILL.md".
const key = `/${posix.normalize(rel.split("\\").join("/"))}`;
const content = await readFile(filePath, "utf8");
await store.put([...SKILLS_SHARED_NAMESPACE], key, createFileData(content));
}
}
/** Copy shared skill files from the store into the sandbox before each agent run. */
function createSkillSandboxSyncMiddleware(backend: CompositeBackend) {
return createMiddleware({
name: "SkillSandboxSyncMiddleware",
beforeAgent: async (state, runtime) => {
const store = (runtime as any).store;
if (!store) {
throw new Error(
"Store is required for syncing skills into the sandbox. " +
"Pass `store` to createDeepAgent and ensure your runtime provides it.",
);
}
const encoder = new TextEncoder();
const files: Array<[string, Uint8Array]> = [];
for (const item of await store.search([...SKILLS_SHARED_NAMESPACE])) {
const normalized = normalizeSkillsStoreKey(String(item.key));
const data = item.value as FileData;
// CompositeBackend routes paths and batches uploads to the right backend.
files.push([
`/skills${normalized}`,
encoder.encode(data.content.join("\n")),
]);
}
if (files.length > 0) await backend.uploadFiles(files);
return state;
},
});
}
async function main() {
const store = new InMemoryStore();
await seedSkillStore(store);
const client = new SandboxClient();
const lsSandbox = await client.createSandbox();
const backend = new CompositeBackend(
new LangSmithSandbox({ sandbox: lsSandbox }),
{
"/skills/": new StoreBackend({
store,
namespace: () => [...SKILLS_SHARED_NAMESPACE],
} as any),
},
);
try {
const agent = await createDeepAgent({
model: "ollama:north-mini-code-1.0",
backend,
skills: ["/skills/"],
store,
middleware: [createSkillSandboxSyncMiddleware(backend)],
});
} finally {
await client.deleteSandbox(lsSandbox.name);
}
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});Open a public LangSmith run for this example.
For a complete example that seeds both skills and memories before execution and syncs both back afterward, see syncing skills and memories with custom middleware.
Troubleshooting
Use LangSmith traces to debug skill discovery, read_file calls on SKILL.md, and supporting resource access. Follow the tracing quickstart to get set up. We recommend you also set up LangSmith Engine, which monitors your traces, detects issues, and proposes fixes.
Skill not activated
Problem: The agent handles the task without reading the skill's SKILL.md.
Solutions:
- Make the description more specific. The agent selects skills from the
descriptionfield alone at discovery. Include what the skill does, when to use it, and keywords the agent can match:
# Good
description: >-
Search the arXiv preprint repository for research papers. Use when the
user asks about academic papers, recent research, or scientific literature.
# Poor
description: Helps with research.Reduce overlap between skills. If multiple skills have similar descriptions, the agent may skip the right one or pick the wrong one. Differentiate descriptions or consolidate related skills.
Confirm the skill is in the
skillsarray. Skills load only from paths you pass at agent creation or from subagent-specificskillsparameters.Pin the skill. When the user or your app already knows which skill applies, pin it. The agent then gets the skill's instructions without having to choose the skill.
Skills missing at startup
Problem: The agent does not list a skill in its system prompt, or read_file on SKILL.md fails.
Solutions:
Check the skill path. Paths must use forward slashes and be relative to the backend root. With
FilesystemBackend, the path is relative toroot_dir. WithStateBackend, pass skill files ininvoke(files={...})usingcreate_file_data().Validate
SKILL.mdfrontmatter. Thenamemust match the parent directory name and follow the Agent Skills specification. Use theskills-refvalidation tool to check formatting.Check file size. Deep Agents skips
SKILL.mdfiles over 10 MB during discovery.Check layered sources. When the same skill name appears in multiple sources, the last source wins. An older or empty skill from a later path can override the one you expect.
Skill changes not picked up
Problem: The agent keeps using an earlier version of a skill after you add, edit, or delete one.
Solution: Skills load once per thread, so a thread that has already run does not see the change. Reset the stored metadata. See Reload skills.
Supporting files not found
Problem: The agent reads SKILL.md but cannot access scripts, references, or assets.
Solutions:
Reference files from
SKILL.md. The agent does not auto-discover supporting files. State what each file contains and when to use it. Use relative paths from the skill root.Keep paths within the skill directory. File paths resolve against the backend. Confirm supporting files exist at the paths your instructions reference.
Sync skills into sandboxes. If you use sandbox backends, skill files outside the container are not available until you copy them in. See Sandbox scripts and syncing skills and memories with custom middleware.
Skill tool not available
Problem: The agent cannot call a tool that a skill lists in include_tools.
Solutions:
Check that the agent read the skill. Skill tools appear only after the agent reads
SKILL.md, and disappear when summarization drops that read. See Add tools to skills.Write
include_toolsas a string. Separate names with spaces. A YAML list never matches a tool.Check the names. Each name must match a tool passed to the agent or to
createSkillsMiddleware, or a name your resolver handles.
Scripts fail to run
Problem: The agent reads a script but cannot run it.
Solution: The agent can read scripts from any backend, but running them requires a sandbox backend. See Execute code with skills.
Subagent cannot access a skill
Problem: A custom subagent does not see skills that the main agent uses.
Solution: Custom subagents do not inherit the main agent's skills. Add a skills parameter to each subagent definition with that subagent's skill source paths. The general-purpose subagent inherits skills from create_deep_agent automatically.
Reference
Skills, memory, and tools
Skills, memory (AGENTS.md files), and tools all provide context or capabilities to the agent. The following table summarizes when to reach for each:
| Skills | Memory | Tools | |
|---|---|---|---|
| Purpose | On-demand capabilities discovered through progressive disclosure | Persistent context loaded at startup | Programmatic actions the agent can call |
| Loading | Read only when the agent determines relevance | Loaded at agent start | Available every turn |
| Format | SKILL.md in named directories | AGENTS.md files | Functions bound to the agent |
| Layering | User, then project (last wins) | User, then project (combined) | Defined at agent creation |
| Use when | Instructions are task-specific and potentially large | Context is always relevant (project conventions, preferences) | The agent needs a programmatic action, or does not have access to the file system |
These are guidelines, not hard boundaries. In practice, skills and memory sit on a spectrum. An agent can update its own skills as it works, capturing new procedures and refining instructions over time. In this way, skills can function as a form of progressive-disclosure memory: context the agent builds up and retrieves on demand rather than loading on every prompt.
Frontmatter fields
The Agent Skills specification defines the following frontmatter fields:
| Field | Required | Description |
|---|---|---|
name | Yes | Lowercase alphanumeric with hyphens, 1-64 characters. Must match the parent directory name. |
description | Yes | What the skill does and when to use it. Max 1,024 characters. |
license | No | License name or reference to a bundled license file. |
compatibility | No | Environment requirements (system packages, network access). Max 500 characters. |
metadata | No | Arbitrary key-value pairs for additional properties. |
allowed-tools | No | Space-separated list of pre-approved tools the skill can use. Experimental. |
---
name: langgraph-docs
description: Use this skill for requests related to LangGraph in order to fetch relevant documentation to provide accurate, up-to-date guidance.
license: MIT
compatibility: Requires internet access for fetching documentation URLs
metadata:
author: langchain
version: "1.0"
allowed-tools: fetch_url
---
# langgraph-docs
Instructions for the agent go here. See [Usage](#usage) for a complete example of skill instructions.WARNING
Refer to the full Agent Skills specification for detailed constraints and validation rules. In Deep Agents, SKILL.md files must be under 10 MB. Files exceeding this limit are skipped during skill loading.
For more example skills, see Deep Agents example skills.
Connect these docs to your agent of choice via MCP for real-time answers.
