Building a Skill-Powered Agent
In this tutorial, you'll build an AI agent that discovers, activates, and follows SKILL.md-driven instructions at runtime — all in about 15 minutes. By the end, your agent will select and use skills autonomously to complete user tasks.
What You'll Build
A skill-powered coding assistant that can:
- Discover available skills from configured directories
- Show skill summaries in its system prompt
- Activate a skill on demand and follow its instructions
- Load skill resources (references, templates, scripts)
- Respect trust boundaries for script access
Prerequisites
- Node.js 18+
- An OpenAI API key (or any LangChain-compatible LLM)
- Basic TypeScript knowledge
- Familiarity with AgentForge tools and agent patterns
Step 1: Set Up the Project
npx @agentforge/cli create skill-agent
cd skill-agent
pnpm installInstall the LLM provider and tools:
pnpm add @agentforge/core @agentforge/skills @agentforge/patterns @agentforge/tools @langchain/openaiCreate .env:
OPENAI_API_KEY=your-api-key-hereStep 2: Create Your First Skill
Skills are directories containing a SKILL.md file with YAML frontmatter. Create a skill root and your first skill:
mkdir -p .agentskills/code-reviewCreate .agentskills/code-review/SKILL.md:
---
name: code-review
description: Performs thorough code reviews with focus on best practices, security, and maintainability.
allowed-tools:
- file-reader
- file-search
---
# Code Review Skill
## Process
1. Read the file(s) the user wants reviewed
2. Analyze for:
- Security vulnerabilities
- Performance issues
- Code style and readability
- Error handling gaps
3. Provide a structured review with severity levels:
- 🔴 Critical — must fix before merge
- 🟡 Warning — should fix
- 🟢 Suggestion — nice to have
## Output Format
Use this template for each finding:
**[SEVERITY] Title**
- File: `path/to/file`
- Line: N
- Issue: Description of the problem
- Fix: Suggested solutionSkill Name Must Match Directory
The name field in the frontmatter must match the parent directory name. So code-review/SKILL.md requires name: code-review.
Step 3: Create a Second Skill with Resources
Create a test-generator skill with a reference template:
mkdir -p .agentskills/test-generator/referencesCreate .agentskills/test-generator/SKILL.md:
---
name: test-generator
description: Generates comprehensive test suites following project conventions and testing best practices.
allowed-tools:
- file-reader
- file-writer
---
# Test Generator Skill
## Process
1. Read the source file to understand the API surface
2. Load the test template from `references/test-template.md`
3. Generate tests covering:
- Happy path for each public method
- Edge cases (null, empty, boundary values)
- Error handling paths
4. Follow the project's existing test conventions
## Conventions
- Use `describe`/`it` blocks with clear descriptions
- One assertion per test when possible
- Use factory helpers for test data setupCreate .agentskills/test-generator/references/test-template.md:
# Test Template
Use this structure for generated test files:
\`\`\`typescript
import { describe, it, expect } from 'vitest';
describe('<ModuleName>', () => {
describe('<methodName>()', () => {
it('should <expected behavior> when <condition>', () => {
// Arrange
// Act
// Assert
});
});
});
\`\`\`Step 4: Configure the Skill Registry
Now wire the skills into your agent. Create src/index.ts:
import { SkillRegistry } from '@agentforge/skills';
import { createReActAgent } from '@agentforge/patterns';
import { ChatOpenAI } from '@langchain/openai';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// 1. Create the skill registry
const skillRegistry = new SkillRegistry({
skillRoots: [
{
path: path.join(__dirname, '..', '.agentskills'),
trust: 'workspace', // Full trust for your own skills
}
],
enabled: true, // Enable skill prompt generation
maxDiscoveredSkills: 20, // Cap to manage prompt token usage
});
console.log(`Discovered ${skillRegistry.size()} skills:`);
for (const skill of skillRegistry.getAll()) {
console.log(` - ${skill.metadata.name}: ${skill.metadata.description}`);
}Run it to verify discovery works:
pnpm dlx tsx src/index.tsExpected output:
Discovered 2 skills:
- code-review: Performs thorough code reviews with focus on best practices, security, and maintainability.
- test-generator: Generates comprehensive test suites following project conventions and testing best practices.Step 5: Generate the System Prompt
The generatePrompt() method produces an <available_skills> XML block that tells the LLM which skills are available:
// 2. Generate skill-aware system prompt
const skillPrompt = skillRegistry.generatePrompt();
console.log(skillPrompt);Output:
<available_skills>
<skill>
<name>code-review</name>
<description>Performs thorough code reviews with focus on best practices, security, and maintainability.</description>
<location>/path/to/.agentskills/code-review</location>
</skill>
<skill>
<name>test-generator</name>
<description>Generates comprehensive test suites following project conventions and testing best practices.</description>
<location>/path/to/.agentskills/test-generator</location>
</skill>
</available_skills>Subset Filtering
You can pass a skills filter to only include specific skills in the prompt:
const prompt = skillRegistry.generatePrompt({ skills: ['code-review'] });This is useful when building focused agents that only need a subset of available skills.
Step 6: Wire Activation Tools into the Agent
The toActivationTools() method returns two tools pre-wired to the registry:
activate-skill— loads the full SKILL.md instructions for a skill by nameread-skill-resource— reads a resource file from a skill's directory
// 3. Get activation tools + file tools that skills reference
import { createFileReaderTool, createFileSearchTool } from '@agentforge/tools';
const [activateSkill, readSkillResource] = skillRegistry.toActivationTools();
const fileReader = createFileReaderTool();
const fileSearch = createFileSearchTool();
// 4. Create the LLM
const llm = new ChatOpenAI({
modelName: 'gpt-4o',
temperature: 0,
});
// 5. Build the agent with skill tools + file tools
const agent = createReActAgent({
model: llm,
tools: [activateSkill, readSkillResource, fileReader, fileSearch],
systemPrompt: `You are a coding assistant with access to specialized skills.
${skillPrompt}
When a user asks for help, check if an available skill matches the task.
If so, use the activate-skill tool to load the skill's full instructions,
then follow those instructions to complete the task.
Use read-skill-resource to load any referenced templates or files.`,
});Step 7: Run Your Skill-Powered Agent
Add the invocation and run it:
// 6. Invoke the agent
const result = await agent.invoke({
messages: [
{
role: 'user',
content: 'Please review the code in src/index.ts for any issues.',
},
],
});
console.log('Agent response:');
for (const msg of result.messages) {
if (msg._getType() === 'ai' && msg.content) {
console.log(msg.content);
}
}The agent will:
- See
code-reviewin<available_skills>and recognize it matches the task - Call
activate-skillwith{ name: "code-review" }to load the full instructions - Follow the skill's review process (read files, analyze, structured output)
pnpm dlx tsx src/index.tsStep 8: Add Observability with Events
The SkillRegistry emits events you can monitor for observability:
import { SkillRegistryEvent } from '@agentforge/skills';
// Listen for skill activations
skillRegistry.on(SkillRegistryEvent.SKILL_ACTIVATED, (data) => {
const event = data as { name: string; skillPath: string; bodyLength: number };
console.log(`📋 Skill activated: ${event.name} (${event.bodyLength} chars)`);
});
// Listen for resource loads
skillRegistry.on(SkillRegistryEvent.SKILL_RESOURCE_LOADED, (data) => {
const event = data as {
name: string;
resourcePath: string;
contentLength: number;
};
console.log(
`📄 Resource loaded: ${event.name}/${event.resourcePath} (${event.contentLength} chars)`,
);
});
// Listen for trust policy denials
skillRegistry.on(SkillRegistryEvent.TRUST_POLICY_DENIED, (data) => {
const event = data as { name: string; resourcePath: string; reason: string };
console.warn(`🚫 Trust denied: ${event.name}/${event.resourcePath} — ${event.reason}`);
});Step 9: Add Trust Policies for External Skills
When loading skills from external or community sources, use trust levels to control access:
const skillRegistry = new SkillRegistry({
skillRoots: [
{
path: path.join(__dirname, '..', '.agentskills'),
trust: 'workspace', // Your own skills — full access
},
{
path: '/usr/local/share/agentskills',
trust: 'trusted', // Vetted community skills — scripts allowed
},
{
path: path.join(process.env.HOME!, '.agentskills'),
trust: 'untrusted', // Unknown skills — scripts blocked
},
],
enabled: true,
});| Trust Level | Reference Files | Script Files | Use Case |
|---|---|---|---|
workspace | ✅ Allowed | ✅ Allowed | First-party skills in your project |
trusted | ✅ Allowed | ✅ Allowed | Vetted community/team skills |
untrusted | ✅ Allowed | ❌ Blocked | Unknown or unreviewed skill sources |
Script Access
Resources under scripts/ directories are subject to trust policy enforcement. Only workspace and trusted roots allow script access. Use allowUntrustedScripts: true in config to override (not recommended for production).
Complete Working Example
Here's the full src/index.ts putting it all together:
import { SkillRegistry, SkillRegistryEvent } from '@agentforge/skills';
import { createReActAgent } from '@agentforge/patterns';
import { createFileReaderTool, createFileSearchTool } from '@agentforge/tools';
import { ChatOpenAI } from '@langchain/openai';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// 1. Create and configure skill registry
const skillRegistry = new SkillRegistry({
skillRoots: [
{
path: path.join(__dirname, '..', '.agentskills'),
trust: 'workspace',
},
],
enabled: true,
maxDiscoveredSkills: 20,
});
// 2. Add observability
skillRegistry.on(SkillRegistryEvent.SKILL_ACTIVATED, (data) => {
const event = data as { name: string; bodyLength: number };
console.log(`📋 Skill activated: ${event.name} (${event.bodyLength} chars)`);
});
skillRegistry.on(SkillRegistryEvent.SKILL_RESOURCE_LOADED, (data) => {
const event = data as { name: string; resourcePath: string };
console.log(`📄 Resource loaded: ${event.name}/${event.resourcePath}`);
});
// 3. Generate prompt and tools
const skillPrompt = skillRegistry.generatePrompt();
const [activateSkill, readSkillResource] = skillRegistry.toActivationTools();
const fileReader = createFileReaderTool();
const fileSearch = createFileSearchTool();
// 4. Create agent
const llm = new ChatOpenAI({ modelName: 'gpt-4o', temperature: 0 });
const agent = createReActAgent({
model: llm,
tools: [activateSkill, readSkillResource, fileReader, fileSearch],
systemPrompt: `You are a coding assistant with access to specialized skills.
${skillPrompt}
When a user asks for help, check if an available skill matches the task.
If so, use the activate-skill tool to load the skill's full instructions,
then follow those instructions to complete the task.
Use read-skill-resource to load any referenced templates or files.`,
});
// 5. Run the agent
const result = await agent.invoke({
messages: [
{
role: 'user',
content: 'Generate tests for the file src/utils/helpers.ts',
},
],
});
for (const msg of result.messages) {
if (msg._getType() === 'ai' && msg.content) {
console.log(msg.content);
}
}What's Next?
- Agent Skills Integration Guide — Configuration reference, runtime flow, security, and rollout checklist
- Skill Authoring Guide — How to write spec-compliant SKILL.md files with frontmatter, resources, and trust policies
- Agent Skills Examples — Common patterns and code snippets for skill integration
- SkillRegistry API Reference — Full API documentation for the SkillRegistry class