Blog

Luis Majano

April 03, 2026

Spread the word


Share your thoughts

BoxLang AI 3.0 Series Β· Part 1 of 7


Every AI framework eventually hits the same wall: your system prompts start drifting. Agent A has a slightly different version of the SQL rules than Agent B. The tone policy on your support bot is three weeks behind the tone policy on your documentation bot. Someone copy-pasted the wrong version. Nobody noticed.

This isn't a discipline problem: it's an architecture problem. System prompts are plain strings, and plain strings don't have a source of truth.

BoxLang AI 3.0 fixes this with the AI Skills system β€” a first-class implementation of Anthropic's Agent Skills open standard that treats knowledge as a first-class, versioned, reusable asset. Define it once. Inject it everywhere. Let your codebase β€” not copy-paste β€” be the source of truth.


🧠 What Is a Skill?

A skill is a named block of domain knowledge or instructions that can be injected into any agent or model's system context at runtime. Think of it as a reusable expertise module: a SQL style guide, a tone-of-voice policy, an API cheat sheet, a set of security rules.

The core class is AiSkill.bx. Each skill has three fields:

// From AiSkill.bx
property name="name"        type="string" default="";
property name="description" type="string" default="";
property name="content"     type="string" default="";

That's it. The description tells the LLM when to apply the skill. The content is the full instruction block. Simple by design.


πŸ“„ The SKILL.md File Format

Skills live in named subdirectories under .ai/skills/, following the Agent Skills open standard:

.ai/skills/
    sql-optimizer/
        SKILL.md
    company-tone/
        SKILL.md
    api-guidelines/
        SKILL.md

The file format is plain Markdown with optional YAML frontmatter:

---
description: Enforces our SQL coding standards. Apply when writing or reviewing any database query.
---

# SQL Coding Standards

Always use snake_case for column and table names.
Prefer CTEs over nested sub-queries for readability.
Never use `SELECT *` β€” list columns explicitly.
Alias all tables with a meaningful short name.
Use parameterized queries for all user input.

One important detail from the source code: if you omit the frontmatter description, BoxLang automatically uses the first paragraph of the body as the description. This matches the Claude Agent Skills standard, and it means even the simplest possible SKILL.md β€” just a few lines of plain text β€” works without any configuration:

// From AiSkill.bx β€” fromPath() method
var descFromFrontmatter = parsed.frontmatter.description ?: ""
if ( descFromFrontmatter.len() ) {
    skill.setDescription( descFromFrontmatter )
} else {
    var bodyText       = parsed.body.trim()
    var blankAt        = bodyText.find( char( 10 ) & char( 10 ) )
    var firstParagraph = blankAt > 0 ? bodyText.left( blankAt - 1 ).trim() : bodyText
    skill.setDescription( firstParagraph )
}

The directory name becomes the skill's default name when loaded from a path. So sql-optimizer/SKILL.md becomes the sql-optimizer skill automatically.


πŸ”§ Creating Skills

Three ways to create skills, for three different use cases.

From a single file:

// Load one skill by path
apiSkill = aiSkill( ".ai/skills/api-guidelines/SKILL.md" )

From an entire directory (recursive by default):

// Discover every SKILL.md under .ai/skills/ and all subdirectories
allSkills = aiSkill( ".ai/skills/", recurse: true )

Inline, for short guidance that lives in your code:

sqlStyle = aiSkill(
    name        : "sql-style",
    description : "SQL coding standards for all database queries",
    content     : "Always use snake_case. Prefer CTEs. Never use SELECT *."
)

The aiSkill() BIF handles all three cases β€” you pass either a path or named arguments, and it figures out the rest.


⚑ Two Injection Modes

This is where the architecture gets genuinely clever. Skills support two injection strategies that you can mix freely within the same agent.

Always-On Skills

Full content injected into the system message on every single call. Zero latency β€” the LLM always has this knowledge in context.

agent = aiAgent(
    name   : "support-bot",
    skills : [
        aiSkill( name: "tone",   content: "Always be warm, concise, and empathetic." ),
        aiSkill( name: "format", content: "Use bullet lists for steps. Keep replies under 300 words." )
    ]
)

Best for: short, universally relevant guidance that applies to virtually every query.

Lazy / Available Skills

Only a compact index β€” the skill name and one-line description β€” is included in the system message. When the LLM determines it needs a skill, it calls a built-in loadSkill( name ) tool to fetch the full content on demand.

agent = aiAgent(
    name            : "code-assistant",
    availableSkills : aiSkill( ".ai/skills/", recurse: true )
)

What the LLM sees in its system message:

## Available Skills
Call loadSkill(name) to activate when needed:
- sql-optimizer: Enforces our SQL coding standards. Apply when writing or reviewing database queries.
- boxlang-expert: BoxLang idioms and best practices for writing idiomatic BoxLang code.
- api-guidelines: REST API design standards for all new endpoints.
- security-policy: Security rules for handling user data and authentication.

The LLM only pulls full content for skills it actually needs. A query about formatting a date never loads the SQL optimizer. Token usage stays low even with hundreds of skills in the library.

The loadSkill Tool β€” Auto-Registered, Not Magic

One of the cleanest implementation details in the codebase is how lazy skills are wired up. When you add available skills to an agent, it automatically registers a loadSkill tool:

// From AiAgent.bx β€” _registerLoadSkillTool()
var loadSkillTool = aiTool(
    name       : "loadSkill",
    description: "Activate a skill from the Available Skills library...",
    callable   : ( required string name ) => {
        var skill = agentSelf.activateSkill( arguments.name )
        if ( isNull( skill ) ) {
            return "Skill '#arguments.name#' was not found..."
        }
        return skill.toContentBlock()
    },
    autoRegister: false
)

When the LLM calls loadSkill( "sql-optimizer" ), two things happen: the full content is returned as a tool result (so the LLM can use it immediately), and the skill is promoted to always-on for all subsequent calls in that session. The agent learns on the fly what it needs.

Promoting Lazy Skills Mid-Session

You can also promote a skill programmatically at any point:

// User just mentioned they want to work on SQL queries
// Pre-load the skill for the rest of the session
agent.activateSkill( "sql-optimizer" )

🌍 Global Skills Pool

Register skills once at the application level and have them automatically available to every new agent β€” no explicit wiring required.

// In Application.bx or ModuleConfig.bx
aiGlobalSkills().add( aiSkill( ".ai/skills/company-tone/SKILL.md" ) )
aiGlobalSkills().add( aiSkill( ".ai/skills/security-policy/SKILL.md" ) )

// Every agent gets these automatically as available (lazy) skills
agent1 = aiAgent( name: "support-bot" )    // already has company-tone + security-policy
agent2 = aiAgent( name: "code-assistant" ) // ditto

You can also configure global skills statically in boxlang.json:

{
    "modules": {
        "bxai": {
            "settings": {
                "skillsDirectory": ".ai/skills",
                "autoLoadSkills": true
            }
        }
    }
}

With autoLoadSkills: true, any SKILL.md file discovered in skillsDirectory at startup is automatically added to the global pool.


🎨 How Skills Render

AiSkill has two rendering methods that are used differently depending on whether the skill is always-on or lazy.

toIndexLine() β€” the compact one-liner for the Available Skills index:

- sql-optimizer: Enforces our SQL coding standards. Apply when writing or reviewing database queries.

toContentBlock() β€” the full markdown block injected for always-on skills:

#### Skill: sql-optimizer
Enforces our SQL coding standards. Apply when writing or reviewing database queries.

# SQL Coding Standards

Always use snake_case for column and table names.
Prefer CTEs over nested sub-queries for readability.
...

The buildSkillsContent() method on AiBaseRunnable assembles both sections into the final system message block β€” always-on skills rendered in full, available skills as a compact index.


πŸ” Introspection

Both AiAgent and AiModel expose full skill visibility:

config = agent.getConfig()

println( config.activeSkillCount )              // 2  β€” always-on
println( config.availableSkillCount )           // 12 β€” lazy
println( config.skills.activeSkills )           // [{ name, description }, ...]
println( config.skills.availableSkills )        // [{ name, description }, ...]

// Render the combined system-message block for debugging
println( agent.buildSkillsContent() )

The system message is also cached and fingerprinted β€” if nothing has changed since the last call (same description, instructions, skill pools), the cached version is returned without rebuilding:

// From AiAgent.bx β€” _buildSystemMessageFingerprint()
private string function _buildSystemMessageFingerprint() {
    var skillNames = variables.skills.map( s => s.getName() ).toList( "," )
    var availNames = variables.availableSkills.map( s => s.getName() ).toList( "," )
    return hash( variables.description & variables.instructions & skillNames & availNames )
}

Cache invalidation happens automatically when you add or activate skills.


πŸ“‹ Full Skills API Reference

Method / BIFWhereDescription
aiSkill( path \| name, description, content, recurse )Global BIFCreate or discover skills
aiGlobalSkills()Global BIFAccess the global shared skill pool
withSkills( skills )AiModel, AiAgentSet always-on skills
addSkill( skill )AiModel, AiAgentAdd a single always-on skill
withAvailableSkills( skills )AiModel, AiAgentSet the lazy skill pool
addAvailableSkill( skill )AiModel, AiAgentAdd a single lazy skill
activateSkill( name )AiModel, AiAgentPromote a lazy skill to always-on
buildSkillsContent()AiModel, AiAgentRender the combined system-message block
listSkills()AiModel, AiAgentGet active and available skill summaries

πŸš€ Putting It Together

Here's a complete real-world example: a code review agent with a curated skill library. Short, universal skills are always-on. A large specialized library is lazy-loaded on demand.

// Always-on: applies to every single response
toneSkill   = aiSkill( name: "tone",   content: "Be concise, technical, and constructive." )
formatSkill = aiSkill( name: "format", content: "Lead with the issue. Follow with code. End with a one-line summary." )

// Lazy library: loaded on demand based on what the user is reviewing
allLangSkills = aiSkill( ".ai/skills/languages/", recurse: true )

agent = aiAgent(
    name            : "code-reviewer",
    description     : "Expert code reviewer across multiple languages and frameworks",
    skills          : [ toneSkill, formatSkill ],
    availableSkills : allLangSkills
)

// BoxLang review β€” agent loads the boxlang-expert skill automatically
response = agent.run( "Review this BoxLang class for style and correctness: ..." )

// SQL review β€” agent loads sql-optimizer automatically
response = agent.run( "Is this query efficient? SELECT * FROM orders WHERE ..." )

No hardcoded system prompts. No copy-paste. Skills live in files, travel with your codebase, and get reviewed alongside your code.


What's Next

In Part 2, we'll go deep on the Tool System Overhaul β€” BaseTool, ClosureTool, the Global Tool Registry, @AITool annotation scanning, and the built-in now@bxai tool that gives every agent temporal awareness for free.

πŸ“– Full Documentation πŸ“¦Install Today: install-bx-module bx-ai 🫢Professional Support

Add Your Comment

Recent Entries

BoxLang AI Deep Dive β€” Part 2 of 7: Building a Production-Grade AI Tool Ecosystem

BoxLang AI Deep Dive β€” Part 2 of 7: Building a Production-Grade AI Tool Ecosystem

Function calling is where most AI frameworks look deceptively simple on the surface and turn into a mess underneath. You define a tool, pass it to the LLM, and when the LLM calls it β€” who handles the lifecycle? Who fires observability events? Who serializes the result? Who resolves the tool by name when the only thing you have is a string?

Luis Majano
Luis Majano
April 03, 2026