Agent skills
Package reusable agent capabilities as Skills, build a SkillCatalog, and let the LLM activate them on demand via the use_skill tool.
A skill is a named bundle of instructions the LLM can load at runtime. Instead of pushing every capability into a single system prompt, you define discrete skills and let the model decide which one applies. When the model sees a task that matches a skill’s description, it calls use_skill(name="...") and receives the full instructions for that skill.
Flux implements skills against the Agent Skills open standard, so skill files written for Flux work in Claude Code, Cursor, GitHub Copilot, and other tools that follow the same spec.
The Skill class
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. Use when the user asks for a "
"security review or mentions vulnerabilities.",
instructions=(
"You are a security expert. Review the provided code for security issues.\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 found, provide:\n"
"- Severity (Critical/High/Medium/Low)\n"
"- Description of the vulnerability\n"
"- A concrete fix with code\n"
),
allowed_tools=["run_linter"],
)
Skill takes three required fields and two optional ones:
| Field | Required | Description |
|---|---|---|
name | Yes | Skill identifier. Lowercase letters, numbers, and hyphens only. Max 64 characters. No leading/trailing hyphens, no consecutive hyphens (--). |
description | Yes | What the skill does and when to use it. The LLM reads this to decide which skill to activate. Max 1024 characters. |
instructions | Yes | The full instructions returned when the skill is activated. Can be any length. |
allowed_tools | No | List of tool function names the skill expects to use. Validated at agent construction time. Defaults to []. |
metadata | No | Arbitrary dict[str, str] for author, version, or similar tags. Defaults to {}. |
Name validation is strict: Skill(name="My Skill", ...) raises SkillValidationError because the name contains uppercase letters and a space.
Skills from SKILL.md files
You can define skills as SKILL.md files rather than Python objects. This is how the Agent Skills standard is meant to be used — files that work across tools.
A skill lives in its own directory. The directory name should match the name field in the frontmatter:
skills/
├── researcher/
│ └── SKILL.md
└── summarizer/
└── SKILL.md
A SKILL.md file uses YAML frontmatter for metadata and markdown for the instructions body:
---
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
---
Research the given topic thoroughly.
1. Use search_web to find relevant sources on the topic
2. Analyze and cross-reference the results
3. Synthesize findings into a comprehensive summary with key points
Note that allowed-tools is a space-delimited string in YAML (not a list), while the Python Skill constructor uses allowed_tools as a list[str].
Load a single file with Skill.from_file(path). Flux splits on ---, parses the frontmatter with PyYAML, and uses the markdown body as instructions.
Building a SkillCatalog
SkillCatalog is an index over a set of skills. Pass a list directly:
from flux.tasks.ai import Skill, SkillCatalog
catalog = SkillCatalog([security_reviewer, performance_reviewer])
Or point it at a directory to scan all immediate subdirectories for SKILL.md files:
catalog = SkillCatalog.from_directory("./skills")
from_directory skips invalid skill files with a warning instead of raising, so a malformed SKILL.md does not abort startup.
You can also register skills after construction:
catalog = SkillCatalog.from_directory("./skills")
catalog.register(custom_skill)
Duplicate names raise SkillCatalogError. Catalog methods for lookup:
skill = catalog.get("researcher") # raises SkillNotFoundError if missing
skills = catalog.find(["researcher", "summarizer"]) # list, in order
all_skills = catalog.list() # all registered skills
Passing skills to an agent
Pass the catalog to agent() via skills=:
from flux import ExecutionContext, task, workflow
from flux.tasks.ai import SkillCatalog, agent
@task
async def search_web(query: str) -> str:
"""Search the web and return relevant results for a query."""
...
catalog = SkillCatalog.from_directory("./skills")
@workflow
async def research(ctx: ExecutionContext):
assistant = await agent(
"You are a helpful research assistant. Use your skills to complete tasks effectively.",
model="ollama/llama3.2",
tools=[search_web],
skills=catalog,
)
return await assistant(f"Research the topic: {ctx.input['topic']}")
When skills is not None, agent() does three things before building the underlying LLM task:
- Calls
build_skills_preamble(catalog)and appends the result tosystem_prompt. - Calls
build_use_skill(catalog)to create ause_skillFlux task, then appends it to the agent’s tool list. - Validates
allowed_toolsfor every skill in the catalog against the agent’s actual tool list, logging a warning for any mismatch.
Nothing changes for the LLM provider itself — it receives an augmented system prompt and one additional tool.
What the system prompt receives
build_skills_preamble appends a ## Skills section to whatever system prompt you provided:
## Skills
You have skills available. To activate a skill, call the `use_skill`
tool with the skill name. The skill returns detailed instructions
for completing the task — follow them using your available tools.
Available skills:
- researcher: Deep research on a topic using web sources. Use when the task requires gathering information from multiple sources and synthesizing findings.
- summarizer: Summarizes long content into concise bullet points. Use when the user wants a brief overview of lengthy material.
Each skill contributes roughly one line (name + description). The full instructions are not in the prompt — they are returned by use_skill when the model asks for them.
What use_skill looks like to the LLM
use_skill is a regular @task with a single name: str parameter:
@task
async def use_skill(name: str) -> str:
"""Activates a skill by name. Returns the skill's full instructions."""
skill = catalog.get(name)
return skill.instructions
The model calls it like any other tool:
use_skill(name="researcher")
The tool returns the skill’s instructions string. If the model passes an unknown name, SkillNotFoundError is raised; it extends ExecutionError, so it integrates with the agent’s retry and error handling.
Because use_skill is a Flux @task, it appears in the event log with full observability: events, OpenTelemetry spans, and retry tracking.
How the model selects skills
The model reads skill descriptions from the system prompt, decides which skill applies, calls use_skill to load the instructions, then follows those instructions using the tools available to it.
For a task like “Research quantum computing”, a model with a researcher skill would:
- Read the system prompt:
- researcher: Deep research on a topic... - Call
use_skill(name="researcher") - Receive the full instructions back
- Call
search_web(query="quantum computing")as directed by those instructions - Return the synthesized result
The model can also activate multiple skills in a single run. Previously loaded instructions stay in the message history, so the model can sequence skills: load researcher, do the research, load summarizer, summarize the findings.
Allowed tools validation
Skills can declare which tools they expect to use. At agent construction, Flux checks those declarations against the actual tool list and warns about any missing tools:
# Skill declares: allowed_tools=["run_linter"]
# Agent has: tools=[search_web]
# → Warning: Skill 'security-reviewer' declares allowed_tool 'run_linter'
# which is not in the agent's tools list.
This is a warning, not an error. The skill can still be activated; it just may not work as intended if the expected tool is absent. Tool names are matched against func.__name__.
Complete example: code review with Python-defined skills
from flux import ExecutionContext, task, workflow
from flux.tasks.ai import Skill, SkillCatalog, agent
security_skill = Skill(
name="security-reviewer",
description="Reviews code for security vulnerabilities including injection, XSS, "
"authentication flaws, and OWASP Top 10 issues. Use when the user asks "
"for a security review or mentions vulnerabilities.",
instructions=(
"You are a security expert. Review the provided code for security issues.\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 found, provide:\n"
"- Severity (Critical/High/Medium/Low)\n"
"- Description of the vulnerability\n"
"- A concrete fix with code\n"
),
)
performance_skill = Skill(
name="performance-reviewer",
description="Reviews code for performance issues including algorithmic complexity, "
"memory leaks, and N+1 query patterns. Use when the user asks for a "
"performance review or mentions optimization.",
instructions=(
"You are a performance engineering expert. Review the provided code.\n\n"
"Check for:\n"
"1. Algorithmic complexity (O(n^2) or worse)\n"
"2. Unnecessary memory allocations\n"
"3. N+1 query patterns\n"
"4. Missing caching opportunities\n"
"5. Blocking I/O in async contexts\n\n"
"For each issue found, provide:\n"
"- Impact (High/Medium/Low)\n"
"- Description of the bottleneck\n"
"- An optimized alternative with code\n"
),
)
@task
async def run_linter(code: str) -> str:
"""Run static analysis on the provided code and return findings."""
...
@workflow
async def code_review(ctx: ExecutionContext):
catalog = SkillCatalog([security_skill, performance_skill])
reviewer = await agent(
"You are a code review assistant. Use the appropriate skill for the "
"type of review requested.",
model="ollama/llama3.2",
tools=[run_linter],
skills=catalog,
)
code = ctx.input["code"]
review_type = ctx.input.get("review_type", "general")
return await reviewer(f"Review this code ({review_type} review):\n\n```\n{code}\n```")
Error reference
| Exception | Extends | When raised |
|---|---|---|
SkillValidationError | ValueError | Invalid name, missing required fields, malformed SKILL.md |
SkillCatalogError | ValueError | Duplicate skill name on register() |
SkillNotFoundError | ExecutionError | use_skill called with an unknown name |
All three are importable from flux.tasks.ai.skills.