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:

FieldRequiredDescription
nameYesSkill identifier. Lowercase letters, numbers, and hyphens only. Max 64 characters. No leading/trailing hyphens, no consecutive hyphens (--).
descriptionYesWhat the skill does and when to use it. The LLM reads this to decide which skill to activate. Max 1024 characters.
instructionsYesThe full instructions returned when the skill is activated. Can be any length.
allowed_toolsNoList of tool function names the skill expects to use. Validated at agent construction time. Defaults to [].
metadataNoArbitrary 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:

  1. Calls build_skills_preamble(catalog) and appends the result to system_prompt.
  2. Calls build_use_skill(catalog) to create a use_skill Flux task, then appends it to the agent’s tool list.
  3. Validates allowed_tools for 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:

  1. Read the system prompt: - researcher: Deep research on a topic...
  2. Call use_skill(name="researcher")
  3. Receive the full instructions back
  4. Call search_web(query="quantum computing") as directed by those instructions
  5. 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

ExceptionExtendsWhen raised
SkillValidationErrorValueErrorInvalid name, missing required fields, malformed SKILL.md
SkillCatalogErrorValueErrorDuplicate skill name on register()
SkillNotFoundErrorExecutionErroruse_skill called with an unknown name

All three are importable from flux.tasks.ai.skills.