Skills
In shortBuild a 10xGraph graph that loads Agent Skills (SKILL.md) on demand and combines them with normal tools.
- 7 min read
- 17 sections
- Updated
- v0.9.2
- Markdown
Source example: examples/skills/graph.py
What you will build
A graph where one assistant can switch into specialized modes at runtime by loading SKILL.md files from disk. The skills follow the Agent Skills specification, so the same folders also work in Claude Code, Codex and GitHub Copilot.
In this tutorial the agent can:
- answer normal questions directly
- call a regular Python tool like
lookup_order - call the auto-injected
activate_skilltool when a request matches a skill - return to the main loop after the skill content has been loaded
Prerequisites
- Python 3.12 or later
10xgraphinstalledpython-dotenvinstalled- a model key for the provider used by the example
Install the basics:
pip install 10xgraph python-dotenvHow the skills system works
flowchart TD
A[User message] --> B[MAIN agent]
B -->|normal reply| G[END]
B -->|tool call: lookup_order| C[TOOL node]
B -->|tool call: activate_skill| C
C -->|tool result message| B
C -->|skill instructions returned| B
H[skills directory] --> C
The key idea is simple:
- you keep reusable instructions in
SKILL.mdfiles SkillConfigmakes those skills discoverable: each skill’s name and description go into an<available_skills>catalog in the system prompt- 10xGraph injects an
activate_skilltool automatically, plusread_skill_resourcewhen a skill bundles extra files - when the model decides a skill fits, it calls
activate_skill("skill-name") - the skill content comes back as a tool result, wrapped in
<skill_content>tags, and becomes part of the next model turn
Step 1: Create a skills directory
The example stores skills next to the graph file:
examples/skills/
├── graph.py
├── chat.py
└── skills/
├── code-review/
│ └── SKILL.md
├── data-analysis/
│ └── SKILL.md
├── humanizer/
│ └── SKILL.md
└── writing-assistant/
└── SKILL.mdEach skill is just a Markdown file with YAML frontmatter.
Example shape:
---
name: code-review
description: "Perform thorough code reviews, identify bugs, suggest improvements, and explain code quality issues. Use when the user shares code and asks for a review, bug hunt, or quality feedback."
metadata:
triggers: "review my code; find bugs"
tags: "engineering, development"
priority: "10"
---
You are now in CODE REVIEW mode.The frontmatter gives the runtime enough structure to:
- identify the skill (
namemust match the folder name) - tell the model what the skill does and when to use it (
description) - add example requests as hints in the catalog (
metadata.triggers, a 10xGraph extension) - order skills in the catalog (
metadata.priority, highest first)
The specification requires metadata values to be strings, so the triggers are a ;-separated string and the priority is quoted. Check a skill with 10xgraph skills --validate examples/skills/skills.
Step 2 - Point SkillConfig at the directory
The example builds the path like this:
from pathlib import Path
from tenxgraph.core.skills import SkillConfig
SKILLS_DIR = str(Path(__file__).parent / "skills")Then it passes that into the agent:
agent = Agent(
model="google/gemini-2.5-flash",
system_prompt=[...],
tool_node=ToolNode([lookup_order]),
skills=SkillConfig(
skills_dir=SKILLS_DIR,
inject_catalog=True,
hot_reload=True,
),
trim_context=True,
)What these options do:
| Field | Effect |
|---|---|
skills_dir |
Tells 10xGraph where to find SKILL.md files |
inject_catalog=True |
Adds the <available_skills> catalog (name, description, trigger hints) to the prompt |
hot_reload=True |
Re-reads a SKILL.md when it changes on disk, so edits are picked up without a restart |
hot_reload=True is especially useful while authoring skills because you can edit a file and retry without restarting the process.
Step 3 - Combine skills with normal tools
This example is useful because it shows that skills do not replace regular tools.
The graph still exposes a regular support tool:
def lookup_order(order_id: str) -> str:
orders = {
"A-1001": "Shipped, arriving Thursday",
"A-1002": "Processing",
"A-1003": "Delivered",
}
return orders.get(order_id, "Order not found")Then the agent is created with that tool node:
tool_node = ToolNode([lookup_order])When skills are enabled, 10xGraph augments that tool node by injecting activate_skill into it (and read_skill_resource if a skill bundles files). The final tool node therefore contains both kinds of capability:
- hand-written Python tools
- the automatically generated skill tools
The example makes that explicit:
tool_node = agent.get_tool_node()That is the important call. It returns the final tool node after skill tooling has been attached.
Step 4 - Route between the agent and tools
The tutorial uses a standard ReAct loop:
def should_use_tools(state: AgentState) -> str:
if not state.context:
return END
last = state.context[-1]
if last.role == "assistant" and hasattr(last, "tools_calls") and last.tools_calls:
return "TOOL"
if last.role == "tool":
return "MAIN"
return ENDExecution flow:
sequenceDiagram
participant User
participant Main as MAIN agent
participant Tools as TOOL node
participant Files as SKILL.md files
User->>Main: "Review this Python function"
Main->>Tools: call activate_skill("code-review")
Tools->>Files: load code-review/SKILL.md
Files-->>Tools: markdown instructions
Tools-->>Main: tool result containing skill content
Main-->>User: review written using the loaded skill
The same loop also handles regular tools. If the user asks about an order, the agent can call lookup_order instead of activate_skill.
Step 5 - Understand what the model actually sees
With inject_catalog=True, the model gets a compact catalog of skills in the prompt. That helps it decide whether a request like:
review this codeanalyse this datahumanize this textwrite an apology email
should trigger a skill.
When the skill is loaded, the tool returns the full markdown instructions. That means the next assistant turn is grounded in the exact contents of the relevant SKILL.md file. If context trimming later drops that tool result, the agent puts the instructions back into the system prompt on its own.
A useful mental model is:
- the catalog helps the model choose
activate_skilldelivers the full instructionsread_skill_resourcefetches bundled files (references, scripts) only when the instructions point to them- the next assistant step applies those instructions
Step 6 - Compile and run the graph
The example graph is a classic two-node setup:
graph = StateGraph(
context_manager=MessageContextManager(max_messages=20),
)
graph.add_node("MAIN", agent)
graph.add_node("TOOL", tool_node)
graph.add_conditional_edges(
"MAIN",
should_use_tools,
{"TOOL": "TOOL", END: END},
)
graph.add_edge("TOOL", "MAIN")
graph.set_entry_point("MAIN")
app = graph.compile()Run it:
cd examples/skills
python graph.pyOr pass a query directly:
python graph.py "Review this Python code: def add(a,b): return a+b"
python graph.py "Help me write a professional apology email to a client"
python graph.py "Analyse this data: sales=[120,95,140,88,160] by month"
python graph.py "Where is order A-1001?"What to verify
When the example starts, it prints the registered tools. You should see:
lookup_orderactivate_skill
Then test these scenarios:
| Input | Expected behavior |
|---|---|
| code review request | agent loads code-review skill |
| writing request | agent loads writing-assistant skill |
| humanization request | agent loads humanizer skill |
| order status request | agent uses lookup_order instead of a skill |
Why this pattern works well
This design keeps responsibilities separate:
- graph code handles orchestration
- Python tools handle deterministic actions
SKILL.mdfiles hold specialized writing and reasoning instructions
That separation is valuable because non-engineers can often improve a skill file without touching graph wiring.
Common mistakes
- Registering the original
ToolNodeinstead ofagent.get_tool_node(). That drops the injectedactivate_skilltool. - Putting all domain instructions in the base system prompt instead of splitting them into focused skills.
- Forgetting that skill selection is model-driven. The
descriptionmust say when to use the skill; trigger phrases are only hints. - Leaving
hot_reload=Truein a production environment where you want more predictable file loading behavior.
Skills architecture recap
flowchart LR
A[Graph code] --> B[Agent]
C[SkillConfig] --> B
D[SKILL.md files] --> C
B --> E[Injected activate_skill tool]
F[Custom Python tools] --> G[ToolNode]
E --> G
G --> B
Related docs
What you learned
- How 10xGraph discovers
SKILL.mdfiles. - How
SkillConfiginjectsactivate_skill(andread_skill_resource) into the tool node. - How to combine skill loading with normal Python tools in one graph.
Variant: a persistent terminal chat
The repo also ships examples/skills/chat.py, which wraps the same graph in a REPL. Three details matter.
Use one thread_id for the whole session, and reuse it on every call:
thread_id = f"skills-chat-{uuid4().hex[:8]}"
result = app.invoke(
{"messages": [Message.text_message(user_input)]},
config={"thread_id": thread_id, "recursion_limit": 20},
)Detect which skill was loaded by scanning tool messages for the <skill_content name="..."> tag that activate_skill wraps around skill text:
SKILL_CONTENT_RE = re.compile(r'<skill_content name="([^"]+)">')
for msg in result["messages"]:
if msg.role == "tool":
match = SKILL_CONTENT_RE.match(msg.text() or "")
if match:
print(f" >> Skill loaded: {match.group(1)}")Print only the latest assistant message with text, since the result also contains tool messages:
for msg in reversed(result["messages"]):
if msg.role == "assistant" and msg.text():
print(f"\nAssistant: {msg.text()}\n")
breakPass the tools through tool_node=ToolNode([...]) as above. Agent has no tools= parameter. Create the thread ID once, not per message, or the conversation looks stateless. Handle KeyboardInterrupt and EOFError in the loop.
Next step
→ Continue with Testing to add fast deterministic tests around graphs like this one.