Create Your First Custom Skill for Hermes Agent

Why Build Custom Skills?

While the community skill catalog covers many use cases, sometimes you need something specific to your workflow. Custom skills let you:

  • Integrate with internal company APIs
  • Automate domain-specific tasks
  • Connect to niche tools and services
  • Create reusable workflows for your team

Skill Architecture

A Hermes skill is a self-contained module with a standard interface:

my-skill/

β”œβ”€β”€ index.ts # Main skill file

β”œβ”€β”€ package.json # Metadata and dependencies

β”œβ”€β”€ README.md # Documentation

└── test/

└── index.test.ts # Tests

Step 1: Scaffold a New Skill

hermes skill create my-weather-skill

πŸ“¦ Creating skill: my-weather-skill

Directory: ~/.hermes/skills/my-weather-skill/

Created files:

β”œβ”€β”€ index.ts

β”œβ”€β”€ package.json

└── README.md

βœ… Skill scaffolded! Edit index.ts to get started.

Step 2: Define the Skill

Open index.ts and define your skill:

import type { HermesSkill, SkillContext, SkillResult } from 'hermes-agent/skills';

const weatherSkill: HermesSkill = {

// Metadata

name: 'weather-lookup',

version: '1.0.0',

description: 'Get current weather and forecasts for any city',

author: 'Your Name',

// Parameter schema β€” tells Hermes what inputs the skill needs

parameters: {

city: {

type: 'string',

required: true,

description: 'City name (e.g., "Tokyo", "New York")',

},

units: {

type: 'string',

required: false,

default: 'metric',

enum: ['metric', 'imperial'],

description: 'Temperature units',

},

},

// Permissions this skill requires

permissions: ['network'],

// The main execution function

async execute(

params: { city: string; units?: string },

context: SkillContext

): Promise {

const { city, units = 'metric' } = params;

try {

// Call weather API

const apiKey = context.config.get('weather_api_key');

if (!apiKey) {

return {

success: false,

error: 'Weather API key not configured. Run: hermes skill configure weather-lookup',

};

}

const url = https://api.openweathermap.org/data/2.5/weather?q=${city}&units=${units}&appid=${apiKey};

const response = await fetch(url);

if (!response.ok) {

return {

success: false,

error: City "${city}" not found. Check the spelling and try again.,

};

}

const data = await response.json();

const tempUnit = units === 'metric' ? 'Β°C' : 'Β°F';

return {

success: true,

output: Weather in ${data.name}, ${data.sys.country}:

🌑️ Temperature: ${data.main.temp}${tempUnit} (feels like ${data.main.feels_like}${tempUnit})

πŸ’§ Humidity: ${data.main.humidity}%

🌀️ Conditions: ${data.weather[0].description}

πŸ’¨ Wind: ${data.wind.speed} ${units === 'metric' ? 'm/s' : 'mph'},

data: data, // Raw data for other skills to use

};

} catch (error) {

return {

success: false,

error: Failed to fetch weather: ${error.message},

};

}

},

// Optional: configuration setup

async configure(context: SkillContext): Promise {

const apiKey = await context.prompt('Enter your OpenWeatherMap API key:');

context.config.set('weather_api_key', apiKey);

context.log('βœ… API key saved!');

},

};

export default weatherSkill;

Step 3: Understanding the Skill API

SkillContext

The context object gives your skill access to Hermes internals:

interface SkillContext {

// Configuration storage (persisted)

config: {

get(key: string): string | undefined;

set(key: string, value: string): void;

};

// Logging

log(message: string): void;

warn(message: string): void;

error(message: string): void;

// User interaction

prompt(message: string): Promise;

// Agent capabilities

agent: {

ask(question: string): Promise; // Ask the AI model

memory: MemoryStore; // Long-term memory

session: SessionStore; // Session data

};

// File system (sandboxed)

fs: {

read(path: string): Promise;

write(path: string, content: string): Promise;

exists(path: string): Promise;

};

}

SkillResult

Every skill execution returns a SkillResult:

interface SkillResult {

success: boolean;

output?: string; // Human-readable output shown to the user

data?: any; // Structured data for other skills or further processing

error?: string; // Error message if success is false

}

Step 4: Test Your Skill

Manual Testing

# Test the skill directly

hermes skill test weather-lookup --params '{"city": "Tokyo"}'

πŸ§ͺ Testing skill: weather-lookup

Input: { city: "Tokyo" }

Output:

Weather in Tokyo, JP:

🌑️ Temperature: 22°C (feels like 21°C)

πŸ’§ Humidity: 65%

🌀️ Conditions: scattered clouds

πŸ’¨ Wind: 3.6 m/s

βœ… Test passed! (1.2s)

Automated Tests

Create test/index.test.ts:

import { describe, it, expect } from 'vitest';

import weatherSkill from '../index';

describe('weather-lookup', () => {

it('should return weather for a valid city', async () => {

const mockContext = createMockContext({

config: { weather_api_key: 'test-key' },

});

const result = await weatherSkill.execute(

{ city: 'Tokyo' },

mockContext

);

expect(result.success).toBe(true);

expect(result.output).toContain('Tokyo');

});

it('should fail gracefully for invalid cities', async () => {

const mockContext = createMockContext({

config: { weather_api_key: 'test-key' },

});

const result = await weatherSkill.execute(

{ city: 'NotARealCity12345' },

mockContext

);

expect(result.success).toBe(false);

expect(result.error).toContain('not found');

});

it('should require API key configuration', async () => {

const mockContext = createMockContext({ config: {} });

const result = await weatherSkill.execute(

{ city: 'Tokyo' },

mockContext

);

expect(result.success).toBe(false);

expect(result.error).toContain('not configured');

});

});

Run tests:

cd ~/.hermes/skills/my-weather-skill

npm test

Step 5: Install and Use

Local Installation

hermes skill install --local ~/.hermes/skills/my-weather-skill

Now use it naturally in conversation:

> What's the weather in Paris?

πŸ”§ Using skill: weather-lookup

Weather in Paris, FR:

🌑️ Temperature: 18°C (feels like 17°C)

πŸ’§ Humidity: 72%

🌀️ Conditions: light rain

πŸ’¨ Wind: 4.2 m/s

Publishing to the Community

When your skill is ready, publish it:

hermes skill publish

πŸ“¦ Publishing weather-lookup v1.0.0...

Checklist:

βœ… package.json valid

βœ… README.md present

βœ… Tests passing (3/3)

βœ… No security issues

βœ… Permissions declared

Published! Your skill is now available at:

https://skills.hermesagent.sbs/weather-lookup

Best Practices

Error Handling

Always return meaningful error messages:

// ❌ Bad

return { success: false, error: 'Error' };

// βœ… Good

return {

success: false,

error: Failed to connect to ${apiUrl}. Check your internet connection and API key.,

};

Graceful Degradation

If part of the skill fails, return partial results:

const weather = await getWeather(city);     // Succeeded

const forecast = await getForecast(city); // Failed

return {

success: true,

output: Current: ${weather}\n\n⚠️ Forecast unavailable (API limit reached),

};

Security

  • Never hardcode API keys β€” use context.config
  • Validate all inputs before using them
  • Declare all required permissions
  • Sanitize file paths to prevent directory traversal

Next Steps


Last updated: April 17, 2026 Β· Hermes Agent v0.8