Agent Skills¶
Agent skills are reusable instruction bundles that agents can discover and activate on demand. Skills follow the Agent Skills open standard — skills authored for Flux work with Claude Code, Cursor, GitHub Copilot, Gemini CLI, and 30+ other tools.
Basic Usage¶
from flux import workflow, ExecutionContext
from flux.tasks.ai import agent, SkillCatalog
catalog = SkillCatalog.from_directory("./skills")
assistant = agent(
"You are a helpful assistant.",
model="ollama/llama3.2",
tools=[search_web],
skills=catalog,
)
@workflow
async def my_workflow(ctx: ExecutionContext):
return await assistant("Research quantum computing")
The agent sees skill descriptions in its system prompt, calls use_skill to load full instructions when relevant, then follows them using the available tools.
Defining Skills¶
SKILL.md Files¶
A skill is a directory containing a SKILL.md file with YAML frontmatter and markdown instructions:
skills/
├── researcher/
│ └── SKILL.md
├── summarizer/
│ └── SKILL.md
└── code-reviewer/
├── SKILL.md
├── references/
│ └── owasp-checklist.md
└── scripts/
└── lint.py
The SKILL.md format:
---
name: researcher
description: Deep research on a topic using web sources. Use when the task requires gathering information from multiple sources and synthesizing findings.
allowed-tools: search_web read_url
metadata:
author: acme-corp
version: "1.0"
---
Research the given topic thoroughly.
1. Use search_web to find relevant sources on the topic
2. Use read_url to extract content from the most promising results
3. Cross-reference findings across multiple sources
4. Synthesize findings into a comprehensive summary with citations
Frontmatter Fields¶
| Field | Required | Description |
|---|---|---|
name |
Yes | Skill identifier. Max 64 chars, lowercase letters, numbers, and hyphens. Should match the parent directory name (Flux warns on mismatch). |
description |
Yes | What the skill does and when to use it. Max 1024 chars. The LLM uses this to decide which skill to activate. |
allowed-tools |
No | Space-delimited list of tool names the skill expects to use. Validated against the agent's tools at construction time. |
metadata |
No | Arbitrary key-value mapping for additional metadata (author, version, etc.). |
license |
No | Part of the Agent Skills standard. Stored but not consumed by Flux. |
compatibility |
No | Part of the Agent Skills standard. Stored but not consumed by Flux. |
Python-Defined Skills¶
Skills can be constructed directly in Python without a SKILL.md file:
from flux.tasks.ai import Skill
security_reviewer = Skill(
name="security-reviewer",
description="Reviews code for security vulnerabilities including injection, "
"XSS, and OWASP Top 10 issues.",
instructions=(
"You are a security expert. Review the provided code.\n\n"
"Check for:\n"
"1. SQL injection vulnerabilities\n"
"2. Cross-site scripting (XSS)\n"
"3. Authentication and authorization flaws\n"
"4. Sensitive data exposure\n"
"5. Input validation issues\n\n"
"For each issue, provide severity, description, and a fix."
),
allowed_tools=["run_linter"],
)
Python skills are useful when: - You want IDE autocompletion and type checking - Skill instructions are generated dynamically - You don't need cross-tool portability
Name Validation¶
Skill names follow the Agent Skills standard:
- Lowercase letters, numbers, and hyphens only
- Must not start or end with a hyphen
- Must not contain consecutive hyphens (
--) - Maximum 64 characters
- Must match the parent directory name (for
SKILL.mdfiles; warns if mismatched)
SkillCatalog¶
The SkillCatalog discovers, indexes, and provides access to skills.
From a Directory¶
Scans immediate subdirectories for SKILL.md files:
from flux.tasks.ai import SkillCatalog
catalog = SkillCatalog.from_directory("./skills")
# Finds: skills/researcher/SKILL.md, skills/summarizer/SKILL.md, etc.
Invalid skills (missing required fields, malformed YAML) are logged as warnings and skipped.
From a List¶
Construct directly from Skill objects:
from flux.tasks.ai import Skill, SkillCatalog
catalog = SkillCatalog([
Skill(name="alpha", description="...", instructions="..."),
Skill(name="beta", description="...", instructions="..."),
])
Mixed Catalogs¶
Directory discovery and explicit registration can be combined:
catalog = SkillCatalog.from_directory("./skills")
catalog.register(Skill(
name="custom-reviewer",
description="Custom review logic.",
instructions="Review using these criteria...",
))
Lookup¶
skill = catalog.get("researcher") # by name, raises SkillNotFoundError if missing
skills = catalog.find(["researcher", "code-reviewer"]) # multiple by name
all_skills = catalog.list() # all registered skills
Name uniqueness is enforced — registering a skill with a duplicate name raises SkillCatalogError.
Agent Integration¶
Passing Skills to an Agent¶
Pass a SkillCatalog to agent() via the skills parameter:
assistant = agent(
"You are a helpful assistant.",
model="openai/gpt-4o",
tools=[search_web, read_url, lint_code],
skills=catalog,
)
When skills is provided, agent() does three things:
- Appends skill descriptions to the system prompt (~20 tokens per skill)
- Creates a
use_skilltool that the LLM can call to load a skill's full instructions - Validates
allowed_tools— warns if a skill declares tools not present in the agent's tool list
No changes are needed in the provider builders (Ollama, OpenAI, Anthropic) — they receive an augmented system prompt and one additional tool.
How Skill Selection Works¶
The LLM selects skills through a tool-calling mechanism:
1. System prompt includes:
"Available skills:
- researcher: Deep research on a topic using web sources.
- summarizer: Summarizes long content into concise bullet points."
2. User instruction: "Research quantum computing"
3. LLM decides "researcher" is relevant, calls:
use_skill(name="researcher")
4. Tool returns the skill's full instructions:
"Research the given topic thoroughly.
1. Use search_web to find relevant sources..."
5. LLM follows the instructions using available tools:
search_web(query="quantum computing advances 2026")
6. LLM returns the final response
Multi-Skill Stacking¶
The LLM can activate multiple skills in a single run. Previously loaded instructions remain in the message history:
# Agent has both "researcher" and "summarizer" skills
result = await assistant("Research quantum computing and summarize the findings")
# LLM flow:
# 1. Calls use_skill("researcher") → gets research instructions
# 2. Calls search_web(...) → gets results
# 3. Calls use_skill("summarizer") → gets summarization instructions
# 4. Returns summarized research findings
Allowed Tools Validation¶
Skills can declare which tools they expect via allowed-tools (SKILL.md) or allowed_tools (Python). At agent construction time, Flux validates these against the agent's actual tools and warns about any missing ones:
# Skill declares: allowed-tools: search_web read_url
# Agent has: tools=[search_web]
# → Warning: "Skill 'researcher' declares allowed_tool 'read_url' which is not in the agent's tools list."
This is a warning, not an error — the skill may still work without all declared tools. Tool names are matched against func.__name__ since that is what the tool executor uses for dispatch.
Event Tracking¶
Skill activation appears in the Flux event log as regular task events:
TASK_STARTED assistant {"instruction": "Research quantum computing"}
TASK_STARTED use_skill {}
TASK_COMPLETED use_skill "Research the given topic thoroughly..."
TASK_STARTED search_web {"query": "quantum computing"}
TASK_COMPLETED search_web "Results: ..."
TASK_COMPLETED assistant "Quantum computing is..."
The use_skill call is a regular @task — it gets full observability (events, OpenTelemetry spans, retry tracking) for free.
Error Handling¶
Skill Not Found¶
If the LLM calls use_skill with an unknown name, SkillNotFoundError is raised. This extends Flux's ExecutionError, so it integrates with the agent's retry and error handling:
assistant = agent(
"You are a helpful assistant.",
model="ollama/llama3.2",
skills=catalog,
).with_options(retry_max_attempts=3)
# If the LLM hallucinates a skill name, the task fails and retries
Validation Errors¶
Construction-time errors raise SkillValidationError (extends ValueError):
# Missing required fields
Skill(name="", description="test", instructions="test")
# → SkillValidationError: Skill name must not be empty.
# Invalid SKILL.md format
Skill.from_file("bad-skill/SKILL.md")
# → SkillValidationError: Skill file must contain YAML frontmatter delimited by '---'.
Catalog Errors¶
Duplicate names raise SkillCatalogError (extends ValueError):
catalog = SkillCatalog([skill_a, skill_a])
# → SkillCatalogError: Skill 'researcher' is already registered.
Examples¶
Multi-Skill Research Agent¶
from flux import task, workflow, ExecutionContext
from flux.tasks.ai import agent, SkillCatalog
@task
async def search_web(query: str) -> str:
"""Search the web and return relevant results."""
...
catalog = SkillCatalog.from_directory("./skills")
assistant = agent(
"You are a research assistant.",
model="ollama/llama3.2",
tools=[search_web],
skills=catalog,
).with_options(retry_max_attempts=3, timeout=120)
@workflow
async def research(ctx: ExecutionContext):
return await assistant(f"Research: {ctx.input['topic']}")
Code Review Agent with Python Skills¶
from flux import task, workflow, ExecutionContext
from flux.tasks.ai import Skill, SkillCatalog, agent
security = Skill(
name="security-reviewer",
description="Reviews code for security vulnerabilities.",
instructions="Check for SQL injection, XSS, auth flaws...",
)
performance = Skill(
name="performance-reviewer",
description="Reviews code for performance issues.",
instructions="Check for O(n^2), memory leaks, N+1 queries...",
)
@task
async def run_linter(code: str) -> str:
"""Run static analysis on the provided code."""
...
reviewer = agent(
"You are a code review assistant.",
model="openai/gpt-4o",
tools=[run_linter],
skills=SkillCatalog([security, performance]),
).with_options(retry_max_attempts=3, timeout=120)
@workflow
async def review(ctx: ExecutionContext):
return await reviewer(f"Review this code:\n{ctx.input['code']}")
Skills with MCP Tools¶
Skills compose naturally with MCP-discovered tools:
from flux.tasks.ai import agent, SkillCatalog
from flux.tasks.mcp import mcp
catalog = SkillCatalog.from_directory("./skills")
async with mcp("http://localhost:8080/mcp", name="server") as client:
tools = await client.discover()
assistant = agent(
"You are a workflow assistant.",
model="ollama/llama3.2",
tools=list(tools),
skills=catalog,
)
result = await assistant("List available workflows and summarize them")
Skill Reference¶
class Skill:
def __init__(
self,
name: str, # required, validated
description: str, # required
instructions: str, # required
allowed_tools: list[str] | None = None, # optional, defaults to []
metadata: dict[str, str] | None = None, # optional, defaults to {}
): ...
@classmethod
def from_file(cls, path: str) -> Skill: ...
SkillCatalog Reference¶
class SkillCatalog:
def __init__(self, skills: list[Skill] | None = None): ...
@classmethod
def from_directory(cls, path: str) -> SkillCatalog: ...
def register(self, skill: Skill) -> None: ...
def get(self, name: str) -> Skill: ...
def find(self, names: list[str]) -> list[Skill]: ...
def list(self) -> list[Skill]: ...