🎯 Skills Guide

View Source

Skills are reusable instruction and capability packages that inject domain knowledge, tools, and prompt fragments into agents.

Quick Start

# Use built-in skills by group
agent = Nous.new("openai:gpt-4",
  skills: [{:group, :review}]
)

# Use specific skill modules
agent = Nous.new("openai:gpt-4",
  skills: [Nous.Skills.CodeReview, Nous.Skills.Debug]
)

# Load skills from markdown files
agent = Nous.new("openai:gpt-4",
  skill_dirs: ["priv/skills/"]
)

Creating Module-Based Skills

Use the use Nous.Skill macro for ergonomic skill definition:

defmodule MyApp.Skills.ElixirExpert do
  use Nous.Skill, tags: [:elixir, :functional], group: :coding

  @impl true
  def name, do: "elixir_expert"

  @impl true
  def description, do: "Provides Elixir-specific coding guidance"

  @impl true
  def instructions(_agent, _ctx) do
    """
    You are an Elixir expert. When writing Elixir code:
    - Prefer pattern matching over conditionals
    - Use the pipe operator for data transformations
    - Leverage OTP for concurrent and fault-tolerant systems
    """
  end

  # Optional: auto-activate when user input matches
  @impl true
  def match?(input) do
    input = String.downcase(input)
    String.contains?(input, ["elixir", "genserver", "phoenix"])
  end

  # Optional: provide tools
  @impl true
  def tools(_agent, _ctx) do
    [Nous.Tool.from_function(&MyTools.read_file/2)]
  end
end

Required Callbacks

CallbackReturnsPurpose
name/0String.t()Unique skill identifier
description/0String.t()Used for matching and display
instructions/2String.t()Injected into agent system prompt

Optional Callbacks

CallbackReturnsPurpose
tools/2[Tool.t()]Additional tools when skill is active
tags/0[atom()]Categorization (auto-set by use opts)
group/0atom()Skill group (auto-set by use opts)
match?/1boolean()Auto-activation predicate

Walkthrough: Creating a Module-Based Skill

Build a "SQL Expert" skill from scratch that provides database query guidance and a schema-inspection tool.

Step 1: Define the module

defmodule MyApp.Skills.SqlExpert do
  use Nous.Skill, tags: [:sql, :database], group: :coding

  @impl true
  def name, do: "sql_expert"

  @impl true
  def description, do: "SQL query optimization and schema design guidance"

  @impl true
  def instructions(_agent, _ctx) do
    """
    You are a SQL and database expert. When helping with database tasks:

    - Prefer indexed lookups over full table scans
    - Use CTEs for complex queries instead of nested subqueries
    - Always suggest adding appropriate indexes for new queries
    - Warn about N+1 query patterns
    - Recommend EXPLAIN ANALYZE for performance investigation
    """
  end

  @impl true
  def match?(input) do
    input = String.downcase(input)

    String.contains?(input, [
      "sql", "query", "database", "table", "index",
      "join", "schema", "migration"
    ])
  end

  @impl true
  def tools(_agent, _ctx) do
    [Nous.Tool.from_function(&MyApp.Tools.describe_table/2)]
  end
end

Step 2: Register with an agent

agent = Nous.new("openai:gpt-4",
  skills: [MyApp.Skills.SqlExpert]
)

Because match?/1 is implemented, the skill auto-activates when user input contains keywords like "sql" or "query". The instructions/2 text is injected into the system prompt and the describe_table tool becomes available.

Step 3: Verify activation

# The skill activates automatically for matching input
{:ok, result} = Nous.run(agent, "Help me optimize this SQL query")

# Or activate manually via the registry
registry = Nous.Skill.Registry.resolve([MyApp.Skills.SqlExpert])
{instructions, tools, registry} = Nous.Skill.Registry.activate(registry, "sql_expert", agent, ctx)

Creating File-Based Skills

Markdown files with YAML frontmatter:

---
name: api_design
description: RESTful API design best practices
tags: [api, rest, design]
group: planning
activation: auto
allowed_tools: [read_file, grep]
priority: 50
---

When designing APIs:
1. Use nouns for resources, verbs for actions
2. Version your API (v1, v2)
3. Use proper HTTP status codes
4. Paginate list endpoints

Frontmatter Fields

FieldTypeDefaultDescription
namestringfilenameSkill identifier
descriptionstring""Used for matching
tagslist[]Categorization tags
groupstringnilSkill group
activationstring"manual""manual" or "auto"
allowed_toolslistnilRestrict available tools
priorityinteger100Lower = higher priority
model_overridestringnilOverride model for this skill

Walkthrough: Creating a File-Based Skill

Build a "REST API Design" skill as a markdown file.

Step 1: Create the skill file

Create priv/skills/api_design.md:

---
name: api_design
description: RESTful API design conventions and best practices
tags: [api, rest, http]
group: planning
activation: auto
priority: 80
---

When designing or reviewing REST APIs, follow these conventions:

## Resource Naming
- Use plural nouns for collections: `/users`, `/orders`
- Use nested routes for relationships: `/users/:id/orders`
- Avoid verbs in URLs; use HTTP methods instead

## HTTP Methods
- GET for retrieval (idempotent, cacheable)
- POST for creation (returns 201 with Location header)
- PUT for full replacement, PATCH for partial updates
- DELETE for removal (idempotent, returns 204)

## Response Patterns
- Paginate list endpoints with `?page=1&per_page=25`
- Return consistent error shapes: `{"error": {"code": "...", "message": "..."}}`
- Use 422 for validation errors, 409 for conflicts

Step 2: Load the skill

Load it individually or from a directory:

# Single file
{:ok, skill} = Nous.Skill.Loader.load_file("priv/skills/api_design.md")
skill.name        #=> "api_design"
skill.tags        #=> [:api, :rest, :http]
skill.activation  #=> :auto
skill.source      #=> :file

# Or load an entire directory
agent = Nous.new("openai:gpt-4",
  skills: ["priv/skills/"]
)

Step 3: Verify in the registry

registry = Nous.Skill.Registry.resolve(["priv/skills/"])

Nous.Skill.Registry.list(registry)
#=> ["api_design"]

skill = Nous.Skill.Registry.get(registry, "api_design")
skill.group       #=> :planning
skill.status      #=> :loaded

Because activation: auto is set, this skill activates automatically at agent init. Its instructions are injected into the system prompt for every request.

Skill Groups

Organize skills by domain:

GroupPurposeBuilt-in Skills
:codingCode generation, implementationRefactor, ExplainCode, ElixirIdioms, EctoPatterns, OtpPatterns, PhoenixLiveView, PythonFastAPI, PythonTyping, PythonDataScience, PythonUv
:reviewCode quality analysisCodeReview, SecurityScan, PythonSecurity
:testingTest creation and validationTestGen, ElixirTesting, PythonTesting
:debugBug finding and fixingDebug
:gitVersion control operationsCommitMessage
:docsDocumentation generationDocGen
:planningArchitecture and designArchitect, TaskBreakdown

Activate entire groups:

agent = Nous.new("openai:gpt-4",
  skills: [{:group, :review}, {:group, :testing}]
)

Walkthrough: Skill Groups

Organize your own skills into groups by setting the group option, then activate or deactivate the entire group at once.

Step 1: Tag your skills with a group

defmodule MyApp.Skills.InputValidation do
  use Nous.Skill, tags: [:validation], group: :quality

  @impl true
  def name, do: "input_validation"
  @impl true
  def description, do: "Input validation patterns and sanitization"
  @impl true
  def instructions(_agent, _ctx), do: "Validate all user inputs. Sanitize strings..."
end

defmodule MyApp.Skills.ErrorHandling do
  use Nous.Skill, tags: [:errors], group: :quality

  @impl true
  def name, do: "error_handling"
  @impl true
  def description, do: "Consistent error handling and reporting"
  @impl true
  def instructions(_agent, _ctx), do: "Use tagged tuples {:ok, result} | {:error, reason}..."
end

For file-based skills, set group in the frontmatter:

---
name: logging_standards
group: quality
activation: manual
---
Always use structured logging with Logger metadata...

Step 2: Register a group directory

If your skills live in a directory organized by group, register the whole tree:

priv/skills/
 quality/
    input_validation.md
    error_handling.md
    logging_standards.md
 deployment/
     docker_best_practices.md
registry = Nous.Skill.Registry.new()
|> Nous.Skill.Registry.register_directory("priv/skills/")

# Query by group
quality_skills = Nous.Skill.Registry.by_group(registry, :quality)
length(quality_skills)  #=> 3

Step 3: Activate/deactivate a group at runtime

# Activate all :quality skills at once
{results, registry} = Nous.Skill.Registry.activate_group(registry, :quality, agent, ctx)
# results is a list of {instructions, tools} tuples, one per skill

# Deactivate the whole group
registry = Nous.Skill.Registry.deactivate_group(registry, :quality)

You can also use {:group, :quality} in the agent's skills list to register built-in skills for that group:

agent = Nous.new("openai:gpt-4",
  skills: [
    {:group, :quality},           # built-in group (if any)
    "priv/skills/quality/"        # your custom group directory
  ]
)

Activation Modes

ModeDescriptionExample
:manualOnly activated explicitlyDefault for most skills
:autoActivated at agent initAlways-on skills
{:on_match, fn}Activated when user input matches{:on_match, &String.contains?(&1, "review")}
{:on_tag, tags}Activated when agent has matching tags{:on_tag, [:elixir]}
{:on_glob, patterns}Activated for matching file patterns{:on_glob, ["**/*.test.ts"]}

:manual

The default mode. The skill is registered but only activates when you explicitly activate it:

# Define with manual activation (the default)
%Nous.Skill{
  name: "advanced_debugging",
  activation: :manual,
  instructions: "Use systematic binary search to isolate bugs..."
}

# Must activate explicitly
{instructions, tools, registry} =
  Nous.Skill.Registry.activate(registry, "advanced_debugging", agent, ctx)

Use :manual for specialized skills that should only engage when the user or your application logic requests them.

:auto

The skill activates automatically when the agent initializes. Its instructions and tools are available from the first request:

defmodule MyApp.Skills.CodeStyle do
  use Nous.Skill, tags: [:style], group: :coding

  @impl true
  def name, do: "code_style"
  @impl true
  def description, do: "Enforces project code style"
  @impl true
  def instructions(_agent, _ctx), do: "Follow the project style guide..."
end

For file-based skills, set activation: auto in frontmatter:

---
name: code_style
activation: auto
---
Follow the project style guide...

Use :auto for skills that should always be active -- style guides, project conventions, safety rules.

{:on_match, fn}

The skill activates dynamically when user input matches a predicate function. The Skills plugin checks this on every request via before_request:

# Module-based: implement match?/1
defmodule MyApp.Skills.DatabaseExpert do
  use Nous.Skill, tags: [:database], group: :coding

  @impl true
  def name, do: "database_expert"
  @impl true
  def description, do: "Database query and schema guidance"
  @impl true
  def instructions(_agent, _ctx), do: "When working with databases..."

  @impl true
  def match?(input) do
    input = String.downcase(input)
    String.contains?(input, ["sql", "query", "migration", "schema", "ecto"])
  end
end

When match?/1 is defined on a module, Skill.from_module/1 sets the activation to {:on_match, &module.match?/1}.

For inline skills, pass the function directly:

%Nous.Skill{
  name: "docker_expert",
  activation: {:on_match, fn input ->
    input |> String.downcase() |> String.contains?(["docker", "container", "dockerfile"])
  end},
  instructions: "When working with Docker..."
}

{:on_tag, tags}

Activates when the agent has matching tags. Useful for skills that should only apply to certain types of agents:

%Nous.Skill{
  name: "phoenix_patterns",
  activation: {:on_tag, [:elixir, :phoenix]},
  instructions: "Use Phoenix conventions..."
}

{:on_glob, patterns}

Activates when the working context involves files matching the given glob patterns:

%Nous.Skill{
  name: "react_testing",
  activation: {:on_glob, ["**/*.test.tsx", "**/*.spec.tsx"]},
  instructions: "When writing React tests, use React Testing Library..."
}

Loading Skills from Directories

# Via Agent option
agent = Nous.new("openai:gpt-4",
  skill_dirs: ["priv/skills/", "~/.nous/skills/"]
)

# Via Registry API
registry = Nous.Skill.Registry.new()
|> Nous.Skill.Registry.register_directory("priv/skills/")
|> Nous.Skill.Registry.register_directories(["~/.nous/skills/", ".nous/skills/"])

Directories are scanned recursively for .md files. Supports nested organization:

priv/skills/
 review/
    code_review.md
    security_scan.md
 testing/
    tdd.md
 deployment.md

Mixing Skill Sources

agent = Nous.new("openai:gpt-4",
  skills: [
    MyApp.Skills.Custom,              # Module-based
    {:group, :testing},               # Built-in group
    %Nous.Skill{                      # Inline struct
      name: "quick_tip",
      instructions: "Always suggest tests",
      activation: :auto,
      source: :inline,
      status: :loaded
    }
  ],
  skill_dirs: ["priv/skills/"]        # Directory scan
)

Runtime Skill Management

Access the registry during a run via ctx.deps[:skill_registry]:

alias Nous.Skill.Registry

# Activate/deactivate by name
{instructions, tools, registry} = Registry.activate(registry, "code_review", agent, ctx)
registry = Registry.deactivate(registry, "code_review")

# Group operations
{results, registry} = Registry.activate_group(registry, :review, agent, ctx)
registry = Registry.deactivate_group(registry, :review)

# Query
active = Registry.active_skills(registry)
matched = Registry.match(registry, "review this code")
skills = Registry.by_group(registry, :coding)
skills = Registry.by_tag(registry, :elixir)

Skills Plugin Configuration

The Nous.Plugins.Skills plugin bridges skill definitions into the agent lifecycle. It is automatically added when you pass skills: [...] to Nous.Agent.new/2.

Lifecycle

The plugin hooks into four stages:

StageWhat happens
initResolves all skill specs into a registry, auto-activates :auto skills
system_promptCollects instructions from all active skills, injects them into the system prompt
toolsCollects tools from all active skills, adds them to the tool list
before_requestMatches the latest user message against skills with {:on_match, fn} activation, auto-activates matches

How the registry is stored

The Skills plugin stores the registry in ctx.deps[:skill_registry]. This means you can access and modify it from tools or hooks:

# In a tool: read the registry
def list_active_skills(ctx, _args) do
  registry = ctx.deps[:skill_registry]

  if registry do
    active = Nous.Skill.Registry.active_skills(registry)
    Enum.map(active, & &1.name)
  else
    []
  end
end

Activate/deactivate skills at runtime

Since the registry lives in deps, you can modify it from a tool using ContextUpdate:

alias Nous.Tool.ContextUpdate

def activate_skill(ctx, %{"skill_name" => name}) do
  registry = ctx.deps[:skill_registry]
  agent = ctx.deps[:agent]  # if you stored the agent in deps

  case registry do
    nil ->
      {:error, "No skill registry found"}

    registry ->
      {instructions, tools, updated_registry} =
        Nous.Skill.Registry.activate(registry, name, agent, ctx)

      {:ok, %{activated: name, has_instructions: instructions != nil},
       ContextUpdate.new() |> ContextUpdate.set(:skill_registry, updated_registry)}
  end
end

def deactivate_skill(ctx, %{"skill_name" => name}) do
  registry = ctx.deps[:skill_registry]

  case registry do
    nil ->
      {:error, "No skill registry found"}

    registry ->
      updated = Nous.Skill.Registry.deactivate(registry, name)

      {:ok, %{deactivated: name},
       ContextUpdate.new() |> ContextUpdate.set(:skill_registry, updated)}
  end
end

Skill injection into the system prompt

When multiple skills are active, their instructions are joined with --- separators and each is labeled with a ## Skill: <name> header:

## Skill: code_review

You are a code review specialist...

---

## Skill: security_scan

Check for common security vulnerabilities...

Skills are sorted by priority (lower number = higher priority, default is 100). Use the priority field to control ordering when instruction order matters.