Plugin Development Guide
This guide walks you through creating custom plugins for ElizaOS, from basic concepts to advanced patterns.
Overview
ElizaOS plugins extend agent capabilities through a standardized interface. Plugins can add:
- Actions: New ways for agents to respond to user input
- Providers: Context and data sources for agent decision-making
- Evaluators: Message analysis and processing logic
- Services: Shared functionality and integrations
Getting Started
Prerequisites
- Node.js 23.3.0 or higher
- TypeScript knowledge
- Basic understanding of ElizaOS concepts
Plugin Structure
A basic plugin follows this structure:
my-plugin/
├── src/
│ ├── index.ts # Main plugin export
│ ├── actions/ # Action handlers
│ ├── providers/ # Context providers
│ ├── evaluators/ # Message evaluators
│ └── services/ # Background services
├── package.json
├── tsconfig.json
└── README.md
Basic Plugin Template
// src/index.ts
import { Plugin, IAgentRuntime } from '@elizaos/core';
import { myAction } from './actions/myAction';
import { myProvider } from './providers/myProvider';
import { myEvaluator } from './evaluators/myEvaluator';
import { MyService } from './services/myService';
const myPlugin: Plugin = {
name: 'my-plugin',
description: 'My custom ElizaOS plugin',
// Core components
actions: [myAction],
providers: [myProvider],
evaluators: [myEvaluator],
services: [MyService], // Service classes, not instances
// Optional configuration
config: {
apiKey: 'your-api-key-here',
},
// Lifecycle hooks
async init(config: Record<string, string>, runtime: IAgentRuntime) {
console.log('My plugin initialized!');
// Initialize plugin resources
},
};
export default myPlugin;
Creating Actions
Actions define how your plugin responds to user messages.
Action Structure
// src/actions/myAction.ts
import { Action, IAgentRuntime, Memory, State } from '@elizaos/core';
export const myAction: Action = {
name: 'MY_ACTION',
description: 'Performs a custom action',
// Validation function - determines if this action should handle the message
validate: async (runtime: IAgentRuntime, message: Memory, state?: State) => {
// Return true if this action should handle the message
return message.content.text.toLowerCase().includes('do something');
},
// Handler function - executes the action
handler: async (runtime: IAgentRuntime, message: Memory, state?: State) => {
// Perform your action logic
const result = await performCustomLogic(message);
// Return response
return {
text: `Action completed: ${result}`,
action: 'MY_ACTION',
};
},
// Examples for training/testing
examples: [
[
{
user: '{{user}}',
content: { text: 'Can you do something special?' },
},
{
user: '{{agent}}',
content: { text: "I'll do something special for you!", action: 'MY_ACTION' },
},
],
],
};
Advanced Action Features
export const advancedAction: Action = {
name: 'ADVANCED_ACTION',
description: 'Demonstrates advanced action features',
// Similarity threshold for matching
similes: ['similar action', 'related command'],
validate: async (runtime: IAgentRuntime, message: Memory) => {
// Access services
const customService = runtime.getService('custom-service');
// Check permissions
const hasPermission = await customService.checkPermission(message.entityId);
// Complex validation logic
return hasPermission && isValidCommand(message.content.text);
},
handler: async (runtime: IAgentRuntime, message: Memory, state?: State) => {
// Access character configuration
const character = runtime.character;
// Use providers for context
const context = await runtime.getProvider('custom-context');
// Access conversation state
const previousMessages = await runtime.messageManager.getMemories({
roomId: message.roomId,
count: 10,
});
// Perform action with full context
const response = await processWithContext(message, context, previousMessages);
// Store action result in state
if (state) {
state.data.lastAction = {
type: 'ADVANCED_ACTION',
timestamp: Date.now(),
result: response,
};
}
return response;
},
};
Creating Providers
Providers supply dynamic context to agents.
Basic Provider
// src/providers/myProvider.ts
import { Provider, IAgentRuntime, Memory, State } from '@elizaos/core';
export const myProvider: Provider = {
get: async (runtime: IAgentRuntime, message?: Memory, state?: State) => {
// Gather contextual information
const data = await fetchRelevantData();
// Format for agent consumption
return `Current context: ${JSON.stringify(data)}`;
},
};
Advanced Provider with Caching
export const cachedProvider: Provider = {
get: async (runtime: IAgentRuntime, message?: Memory) => {
const cacheKey = `provider-${message?.roomId || 'global'}`;
const cacheService = runtime.getService('cache');
// Check cache first
const cached = await cacheService.get(cacheKey);
if (cached && Date.now() - cached.timestamp < 60000) {
// 1 minute TTL
return cached.data;
}
// Fetch fresh data
const freshData = await complexDataFetch();
// Update cache
await cacheService.set(cacheKey, {
data: freshData,
timestamp: Date.now(),
});
return freshData;
},
};
Creating Evaluators
Evaluators analyze messages and determine processing paths.
Basic Evaluator
// src/evaluators/myEvaluator.ts
import { Evaluator, IAgentRuntime, Memory, State } from '@elizaos/core';
export const myEvaluator: Evaluator = {
name: 'MY_EVALUATOR',
// Validation - should this evaluator process the message?
validate: async (runtime: IAgentRuntime, message: Memory) => {
return message.content.text.length > 10;
},
// Evaluation logic
handler: async (runtime: IAgentRuntime, message: Memory) => {
// Analyze message
const sentiment = await analyzeSentiment(message.content.text);
const intent = await detectIntent(message.content.text);
// Store evaluation results
await runtime.messageManager.createMemory({
...message,
content: {
...message.content,
evaluations: {
sentiment,
intent,
evaluator: 'MY_EVALUATOR',
},
},
});
// Return evaluation context
return {
sentiment,
intent,
shouldRespond: sentiment.score < -0.5 || intent.type === 'question',
};
},
};
Chained Evaluators
export const chainedEvaluator: Evaluator = {
name: 'CHAINED_EVALUATOR',
handler: async (runtime: IAgentRuntime, message: Memory, state?: State) => {
// Run multiple evaluations in sequence
const evaluations = [];
// Sentiment analysis
if (await shouldAnalyzeSentiment(message)) {
const sentiment = await analyzeSentiment(message.content.text);
evaluations.push({ type: 'sentiment', result: sentiment });
}
// Entity extraction
if (await shouldExtractEntities(message)) {
const entities = await extractEntities(message.content.text);
evaluations.push({ type: 'entities', result: entities });
}
// Topic classification
if (await shouldClassifyTopic(message)) {
const topics = await classifyTopics(message.content.text);
evaluations.push({ type: 'topics', result: topics });
}
// Aggregate results
return {
evaluations,
summary: summarizeEvaluations(evaluations),
recommendations: generateRecommendations(evaluations),
};
},
};
Creating Services
Services provide shared functionality across your plugin.
Basic Service
// src/services/myService.ts
import { Service, IAgentRuntime } from '@elizaos/core';
export class MyService extends Service {
static serviceType = 'my-service';
capabilityDescription = 'Provides custom service functionality';
constructor(runtime?: IAgentRuntime) {
super(runtime);
}
static async start(runtime: IAgentRuntime): Promise<MyService> {
const service = new MyService(runtime);
await service.initialize();
return service;
}
async stop(): Promise<void> {
// Cleanup resources
await this.closeConnections();
}
private async initialize(): Promise<void> {
// Initialize service resources
await this.setupConnections();
}
// Service methods
async performOperation(data: any): Promise<any> {
// Service logic
return this.processData(data);
}
private async setupConnections(): Promise<void> {
// Setup logic
}
private async closeConnections(): Promise<void> {
// Cleanup logic
}
private processData(data: any): any {
// Data processing logic
return data;
}
}
Advanced Service with State Management
export class StatefulService extends Service {
private state: Map<string, any> = new Map();
private subscriptions: Map<string, Function[]> = new Map();
// State management
setState(key: string, value: any): void {
const oldValue = this.state.get(key);
this.state.set(key, value);
// Notify subscribers
this.notifySubscribers(key, value, oldValue);
}
getState(key: string): any {
return this.state.get(key);
}
// Subscription management
subscribe(key: string, callback: Function): () => void {
if (!this.subscriptions.has(key)) {
this.subscriptions.set(key, []);
}
this.subscriptions.get(key)!.push(callback);
// Return unsubscribe function
return () => {
const callbacks = this.subscriptions.get(key) || [];
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
};
}
private notifySubscribers(key: string, newValue: any, oldValue: any): void {
const callbacks = this.subscriptions.get(key) || [];
callbacks.forEach((callback) => {
try {
callback(newValue, oldValue);
} catch (error) {
console.error('Subscriber error:', error);
}
});
}
}
Plugin Configuration
Configuration Schema
// src/config.ts
export const configSchema = {
type: 'object',
properties: {
apiKey: {
type: 'string',
description: 'API key for external service',
required: true,
},
endpoint: {
type: 'string',
description: 'Service endpoint URL',
default: 'https://api.example.com',
},
timeout: {
type: 'number',
description: 'Request timeout in milliseconds',
default: 5000,
},
features: {
type: 'object',
properties: {
enableCache: {
type: 'boolean',
default: true,
},
cacheSize: {
type: 'number',
default: 100,
},
},
},
},
};
Using Configuration
const myPlugin: Plugin = {
name: 'my-plugin',
async onLoad(runtime: IAgentRuntime) {
// Get plugin configuration
const config = runtime.getSetting('myPlugin');
// Validate configuration
if (!config?.apiKey) {
throw new Error('API key is required for my-plugin');
}
// Initialize with config
const service = new MyService(config);
await service.initialize();
// Register service
runtime.registerService('my-service', service);
},
};
Testing Plugins
Unit Testing
// tests/myAction.test.ts
import { describe, it, expect, beforeEach } from 'bun:test';
import { createMockRuntime } from '@elizaos/testing';
import { myAction } from '../src/actions/myAction';
describe('myAction', () => {
let runtime: any;
beforeEach(() => {
runtime = createMockRuntime();
});
it('should validate correct messages', async () => {
const message = {
content: { text: 'Please do something special' },
userId: 'test-user',
roomId: 'test-room',
};
const isValid = await myAction.validate(runtime, message);
expect(isValid).toBe(true);
});
it('should handle action correctly', async () => {
const message = {
content: { text: 'Do something now' },
userId: 'test-user',
roomId: 'test-room',
};
const result = await myAction.handler(runtime, message);
expect(result.text).toContain('Action completed');
expect(result.action).toBe('MY_ACTION');
});
});
Integration Testing
// tests/integration.test.ts
import { describe, it, expect } from 'bun:test';
import { createTestAgent } from '@elizaos/testing';
import myPlugin from '../src';
describe('Plugin Integration', () => {
it('should load and function correctly', async () => {
// Create test agent with plugin
const agent = await createTestAgent({
plugins: [myPlugin],
character: {
name: 'TestAgent',
plugins: ['my-plugin'],
},
});
// Send test message
const response = await agent.processMessage({
content: { text: 'Do something special' },
userId: 'test-user',
roomId: 'test-room',
});
// Verify plugin handled the message
expect(response.action).toBe('MY_ACTION');
expect(response.text).toContain('Action completed');
// Cleanup
await agent.cleanup();
});
});
Publishing Plugins
Package Configuration
{
"name": "@your-org/elizaos-plugin-custom",
"version": "1.0.0",
"description": "Custom plugin for ElizaOS",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"test": "bun test",
"prepare": "bun run build"
},
"keywords": ["elizaos", "plugin", "ai", "agent"],
"peerDependencies": {
"@elizaos/core": "^1.0.0"
},
"devDependencies": {
"@elizaos/core": "^1.0.0",
"@elizaos/testing": "^1.0.0",
"typescript": "^5.0.0"
},
"files": ["dist", "README.md", "LICENSE"],
"publishConfig": {
"access": "public"
}
}
Publishing Steps
-
Build your plugin:
bun run build
bun run test -
Publish to npm:
bun publish
-
Document usage:
# Installation
bun add @your-org/elizaos-plugin-custom
# Usage
Add to your character configuration:
```json
{
"plugins": ["@your-org/elizaos-plugin-custom"]
}
```
Best Practices
1. Error Handling
Always handle errors gracefully:
export const robustAction: Action = {
handler: async (runtime, message) => {
try {
const result = await riskyOperation();
return { text: `Success: ${result}` };
} catch (error) {
runtime.logger.error('Action failed', { error, message });
// Graceful fallback
return {
text: 'I encountered an issue processing that request. Please try again.',
error: true,
};
}
},
};
2. Resource Management
Clean up resources properly:
const myPlugin: Plugin = {
onLoad: async (runtime) => {
// Initialize resources
const connection = await createConnection();
runtime.registerService('connection', connection);
},
onUnload: async (runtime) => {
// Cleanup resources
const connection = runtime.getService('connection');
await connection?.close();
},
};
3. Performance Optimization
Cache expensive operations:
const cachedAction: Action = {
handler: async (runtime, message) => {
const cacheKey = `result-${message.content.text}`;
// Check cache
const cached = await runtime.cacheGet(cacheKey);
if (cached) {
return cached;
}
// Compute result
const result = await expensiveOperation(message);
// Cache for 5 minutes
await runtime.cacheSet(cacheKey, result, 300);
return result;
},
};
4. Documentation
Document your plugin thoroughly:
/**
* Weather Plugin for ElizaOS
*
* Provides weather information and forecasts
*
* @example
* ```json
* {
* "plugins": ["@elizaos/plugin-weather"],
* "settings": {
* "weather": {
* "apiKey": "your-api-key"
* }
* }
* }
* ```
*/
export default weatherPlugin;
Advanced Topics
Plugin Composition
Combine multiple plugins:
import { composePlugins } from '@elizaos/core';
import weatherPlugin from './weather';
import newsPlugin from './news';
export default composePlugins(weatherPlugin, newsPlugin);
Dynamic Plugin Loading
Load plugins at runtime:
async function loadDynamicPlugin(pluginPath: string): Promise<Plugin> {
const module = await import(pluginPath);
return module.default;
}
Plugin Communication
Plugins can communicate via services:
// Plugin A
const pluginA: Plugin = {
onLoad: async (runtime) => {
runtime.registerService('plugin-a-service', {
getData: () => 'data from plugin A',
});
},
};
// Plugin B
const pluginB: Plugin = {
actions: [
{
handler: async (runtime, message) => {
const serviceA = runtime.getService('plugin-a-service');
const data = serviceA?.getData();
return { text: `Got: ${data}` };
},
},
],
};
Troubleshooting
Common Issues
- Plugin not loading: Check that the plugin is properly exported and listed in character configuration
- Actions not triggering: Verify validation logic and action names
- Service errors: Ensure services are initialized before use
- Memory leaks: Clean up event listeners and timers in onUnload
Debug Mode
Enable debug logging:
const debugPlugin: Plugin = {
onLoad: async (runtime) => {
if (runtime.getSetting('debug')) {
runtime.logger.level = 'debug';
runtime.logger.debug('Plugin loaded in debug mode');
}
},
};
Resources
Next Steps
- Start with the basic template
- Add actions for user interactions
- Create providers for context
- Build services for shared functionality
- Test thoroughly
- Publish and share with the community