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_skill tool when a request matches a skill
  • return to the main loop after the skill content has been loaded

Prerequisites

  • Python 3.12 or later
  • 10xgraph installed
  • python-dotenv installed
  • a model key for the provider used by the example

Install the basics:

Terminal
pip install 10xgraph python-dotenv

How 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:

  1. you keep reusable instructions in SKILL.md files
  2. SkillConfig makes those skills discoverable: each skill’s name and description go into an <available_skills> catalog in the system prompt
  3. 10xGraph injects an activate_skill tool automatically, plus read_skill_resource when a skill bundles extra files
  4. when the model decides a skill fits, it calls activate_skill("skill-name")
  5. 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:

Text
examples/skills/
├── graph.py
├── chat.py
└── skills/
    ├── code-review/
    │   └── SKILL.md
    ├── data-analysis/
    │   └── SKILL.md
    ├── humanizer/
    │   └── SKILL.md
    └── writing-assistant/
        └── SKILL.md

Each skill is just a Markdown file with YAML frontmatter.

Example shape:

markdown
---
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 (name must 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:

Python
from pathlib import Path
from tenxgraph.core.skills import SkillConfig

SKILLS_DIR = str(Path(__file__).parent / "skills")

Then it passes that into the agent:

Python
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:

Python
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:

Python
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:

Python
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:

Python
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 END

Execution 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 code
  • analyse this data
  • humanize this text
  • write 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_skill delivers the full instructions
  • read_skill_resource fetches 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:

Python
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:

Terminal
cd examples/skills
python graph.py

Or pass a query directly:

Terminal
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_order
  • activate_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.md files 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 ToolNode instead of agent.get_tool_node(). That drops the injected activate_skill tool.
  • Putting all domain instructions in the base system prompt instead of splitting them into focused skills.
  • Forgetting that skill selection is model-driven. The description must say when to use the skill; trigger phrases are only hints.
  • Leaving hot_reload=True in 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

What you learned

  • How 10xGraph discovers SKILL.md files.
  • How SkillConfig injects activate_skill (and read_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:

Python
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:

Python
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:

Python
for msg in reversed(result["messages"]):
    if msg.role == "assistant" and msg.text():
        print(f"\nAssistant: {msg.text()}\n")
        break

Pass 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.

Last updated for v0.9.2Edit this page on GitHubReport an issue