Skills

In shortRun an example graph that loads Agent Skills (SKILL.md files) on demand and combines them with normal Python tools.

  • 6 min read
  • 13 sections
  • Updated
  • v0.10.0
  • Markdown

This example builds one assistant that switches into specialized modes at runtime by loading SKILL.md files from disk, alongside a normal Python tool. The code lives in agentflow/examples/skills/ and needs Python 3.12 or newer. Skills follow the Agent Skills format, so the folders are portable between tools.

What this example shows

The agent in this example:

  • answers normal questions directly
  • calls a regular Python tool for weather lookups
  • loads dynamic SKILL.md files when requests match a skill’s description
  • stays in the main loop after the skill content is injected

How to run it

Install the required packages:

Terminal
pip install "10xgraph[google-genai]" python-dotenv

The scripts call load_dotenv(), so you can also put GOOGLE_API_KEY in a .env file next to where you run them.

Set your Google API key:

Terminal
export GOOGLE_API_KEY=your_key_here

Run the interactive chat:

Terminal
python examples/skills/chat.py

Or run the scripted example with a single query (from agentflow/):

Terminal
cd examples/skills
python graph.py
python graph.py "Review this Python function: def add(a, b): return a + b"
python graph.py "Write a professional apology email to a client"

Example structure

The skills folder is in the repo:

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

Each SKILL.md contains frontmatter and instructions:

skills/code-review/SKILL.md
---
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; check this code; find bugs"
  tags: "engineering"
  priority: "10"
---

You are now in code review mode.
...

How skills are discovered and injected

You point SkillConfig at a folder, and the agent discovers every SKILL.md in it, adds a catalog to the system prompt, and registers the skill tools. The agent is built in the next section; here are the settings that control the behavior.

When the graph is built, 10xGraph adds these tools to the tool node:

  • activate_skill(skill_name) loads a skill’s instructions.
  • read_skill_resource(skill_name, path) reads a file bundled with a skill. It is registered only when a skill bundles files.
Setting Effect
skills_dir Path to the folder containing skill folders
inject_catalog=True Adds skill names and descriptions to the system prompt so the model knows what to load (default True)
hot_reload=True Re-reads a SKILL.md from disk when its modification time changes (default True; useful during authoring)

Building the graph

The example defines a normal tool, an agent with skills, a router and a ReAct-style graph. Together they form the complete graph.py (the real file also prints the registered tools and accepts a query from the command line).

Define the tool, agent and graph

examples/skills/graph.py
from pathlib import Path

from dotenv import load_dotenv

from tenxgraph.core.graph import Agent, StateGraph, ToolNode
from tenxgraph.core.skills import SkillConfig
from tenxgraph.core.state import AgentState, Message
from tenxgraph.core.state.message_context_manager import MessageContextManager
from tenxgraph.utils.constants import END

load_dotenv()


# A regular Python tool, used alongside the skills
def get_weather(location: str) -> str:
    """Get the current weather for a location."""
    weather_data = {
        "london": "Cloudy, 15°C",
        "new york": "Sunny, 22°C",
        "tokyo": "Rainy, 18°C",
        "paris": "Partly cloudy, 17°C",
    }
    location_lower = location.lower()
    if location_lower in weather_data:
        return f"The weather in {location} is: {weather_data[location_lower]}"
    else:
        return f"Weather data not available for {location}. Try London, New York, Tokyo, or Paris."


# The folder that holds one sub-folder per skill
SKILLS_DIR = str(Path(__file__).parent / "skills")

# Registered in the graph as "TOOL"
tool_node = ToolNode([get_weather])

agent = Agent(
    model="google/gemini-2.5-flash",
    system_prompt=[
        {"role": "system", "content": "You are a smart, multi-skilled assistant."}
    ],
    tool_node="TOOL",  # resolved to the ToolNode above when the graph compiles
    skills=SkillConfig(
        skills_dir=SKILLS_DIR,
        inject_catalog=True,  # add the <available_skills> catalog to the prompt
        hot_reload=True,  # re-read a SKILL.md when it changes on disk
    ),
    trim_context=True,
)


def should_use_tools(state: AgentState) -> str:
    """Route to TOOL node if there are tool calls, else END."""
    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

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()

The agent is passed tool_node="TOOL", a string naming the graph node that holds the ToolNode. When the graph compiles, 10xGraph resolves it and wires the skill tools into that node.

Run one query

Invoke the compiled graph with a message and a thread id, then print the assistant replies.

examples/skills/graph.py
if __name__ == "__main__":
    result = app.invoke(
        {"messages": [Message.text_message("Review this function: def add(a, b): return a + b")]},
        config={"thread_id": "skills-demo-1", "recursion_limit": 15},
    )
    for msg in result["messages"]:
        if msg.role == "assistant":
            print(msg.text() or "(no text)")

How the model uses skills

At runtime the model sees an <available_skills> catalog in the system prompt, then calls activate_skill when a request matches a skill’s description. The tool returns the skill body, which the model follows for the rest of the turn.

The catalog lists each skill’s name and description, plus any triggers from the frontmatter, ordered by priority (highest first):

xml
<available_skills>
  <skill>
    <name>code-review</name>
    <description>Perform thorough code reviews, ...</description>
    <triggers>review my code; check this code; ...</triggers>
  </skill>
  ...
</available_skills>

When a request matches, the model calls activate_skill("code-review"). The tool returns the SKILL.md body wrapped in <skill_content> tags, followed by a list of any bundled files:

Text
<skill_content name="code-review">
You are now in **CODE REVIEW** mode.
...
</skill_content>

The result is a tool message in the conversation, so the model applies those instructions to the user’s request.

Testing the example

Start the example and test these inputs:

Text
You: Review this Python function for bugs

The agent should load the code-review skill.

Text
You: Analyse this data: [120, 95, 140, 88, 160]

The agent should load the data-analysis skill.

Text
You: What's the weather in London?

The agent should call the get_weather tool instead of loading a skill. Model behavior varies, so treat these outcomes as typical, not guaranteed. In chat.py a line such as >> Skill loaded: code-review confirms an activation.

The persistent chat variant

examples/skills/chat.py wraps the same graph in a terminal REPL. The excerpts below show the three patterns that matter: one thread per session, detecting which skill loaded, and printing the final reply.

Use one thread for the whole session

examples/skills/chat.py
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

examples/skills/chat.py
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)}")
examples/skills/chat.py
for msg in reversed(result["messages"]):
    if msg.role == "assistant" and msg.text():
        print(f"\nAssistant: {msg.text()}\n")
        break

The chat version passes tools=[get_weather] to the Agent instead of a tool_node, then calls agent.get_tool_node() to get the tool node with the skill tools included and adds it to the graph as “TOOL”.

Key patterns

Skills work because:

  • The <available_skills> catalog helps the model decide when to load a skill
  • activate_skill returns the full SKILL.md content, grounding the model in exact instructions
  • read_skill_resource lets skills bundle references and scripts the model can read
  • Skill activations are recorded in the execution state, so they survive context trimming

Common mistakes

  • Adding the agent to a graph without a node named “TOOL” when you pass tool_node="TOOL"; the name must match the node you add with add_node
  • Putting all instructions in the system prompt instead of splitting them into focused SKILL.md files
  • Writing descriptions that don’t match what users actually type; the catalog is the model’s only guide to when to load a skill
  • Leaving hot_reload=True on in production when you do not need it; it exists for editing skills while the app runs (set hot_reload=False to disable)

Next step

Continue with Testing to add fast unit tests around graphs like this one.

Frequently asked questions

How does the model know which skill to load?
The agent adds an available_skills catalog (name, description and triggers) to the system prompt. When a request matches a description, the model calls activate_skill with that skill name.
Can I mix skills with normal Python tools?
Yes. The example registers get_weather in a ToolNode, and the skill tools are added to the same node, so one agent can call both.
Last updated for v0.10.0Edit this page on GitHubReport an issue