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 a trusted <available_skills> XML block and, when needed, a separate <untrusted_skills> block that keeps community roots discoverable without treating them as implicitly safe:
// 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>
<trust>workspace</trust>
</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>
<trust>workspace</trust>
</skill>
</available_skills>If you also scan unreviewed community roots, they appear in a separate section:
<untrusted_skills>
<trust_notice>Untrusted skills are discoverable, but their full SKILL.md bodies stay blocked until the root is promoted to trusted or workspace status.</trust_notice>
<skill>
<name>community-pack</name>
<description>Third-party community skill</description>
<location>/path/to/community-pack</location>
<trust>untrusted</trust>
<activation_policy>requires-trusted-root</activation_policy>
</skill>
</untrusted_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 trusted/workspace skill by nameread-skill-resource— reads a resource file from a skill's directory
// 3. Get activation tools + file tools that skills reference
import { createModelSafeToolPreset } from '@agentforge/tools';
const [activateSkill, readSkillResource] = skillRegistry.toActivationTools();
const { fileTools, directoryTools } = createModelSafeToolPreset({
fileSystem: { workspaceRoot: process.cwd() },
});
// 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, ...fileTools, ...directoryTools],
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 from trusted or workspace roots,
then follow those instructions to complete the task.
Use read-skill-resource to load any referenced templates or files.`,
});The activation tools intentionally remain separate: read-skill-resource applies the registry's skill-root trust policy to references and scripts. The preset is for model-controlled project paths and requires an explicit workspaceRoot (or allowedRoots); it rejects .. traversal, symlink escapes, and absolute paths outside that boundary. Unrestricted filesystem factories are reserved for trusted local automation. See the repository security policy for the complete boundary model.
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 from the trusted workspace root - 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 | Full Skill Activation | Reference Files | Script Files | Use Case |
|---|---|---|---|---|
workspace | Allowed | Allowed | Allowed | First-party skills in your project |
trusted | Allowed | Allowed | Allowed | Vetted community/team skills |
untrusted | Blocked until root promotion | 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).
Full Skill Activation
Untrusted roots are still listed in the prompt so the model can surface them for operator review, but activate-skill will refuse to return their full SKILL.md bodies until you explicitly promote that root to trusted or workspace.
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 { createModelSafeToolPreset } 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 { fileTools, directoryTools } = createModelSafeToolPreset({
fileSystem: { workspaceRoot: path.join(__dirname, '..') },
});
// 4. Create agent
const llm = new ChatOpenAI({ modelName: 'gpt-4o', temperature: 0 });
const agent = createReActAgent({
model: llm,
tools: [activateSkill, readSkillResource, ...fileTools, ...directoryTools],
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 from trusted or workspace roots,
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