packages/docs/plugins/development.mdx
That's it. No complex setup, no boilerplate to maintain.
<Tip> **30 minutes to your first plugin.** Focus on your logic, not infrastructure. elizaOS plugins work identically across TypeScript, Python, and Rust. </Tip>The easiest way to create a new plugin is using the elizaOS CLI, which provides interactive scaffolding with pre-configured templates.
elizaos createThe CLI offers two plugin templates to get you started quickly:
# Interactive plugin creation
elizaos create
# Or specify the name directly
elizaos create my-plugin --type plugin
When creating a plugin, you'll be prompted to choose between:
Quick Plugin (Backend Only) - Simple backend-only plugin without frontend
Full Plugin (with Frontend) - Complete plugin with React frontend and API routes
After running elizaos create and selecting "Quick Plugin", you'll get:
plugin-my-plugin/
├── src/
│ ├── index.ts # Plugin manifest
│ ├── actions/ # Your agent actions
│ │ └── example.ts
│ ├── providers/ # Context providers
│ │ └── example.ts
│ └── types/ # TypeScript types
│ └── index.ts
├── package.json # Pre-configured with elizaos deps
├── tsconfig.json # TypeScript config
├── build.ts # Build script using Bun.build
└── README.md # Plugin documentation
Selecting "Full Plugin" adds frontend capabilities:
plugin-my-plugin/
├── src/
│ ├── index.ts # Plugin manifest with routes
│ ├── actions/
│ ├── providers/
│ ├── types/
│ └── frontend/ # React frontend
│ ├── App.tsx
│ ├── main.tsx
│ └── components/
├── public/ # Static assets
├── index.html # Frontend entry
├── vite.config.ts # Vite configuration
├── tailwind.config.js # Tailwind setup
└── [other config files]
Once your plugin is created:
# Navigate to your plugin
cd plugin-my-plugin
# Install dependencies (automatically done by CLI)
bun install
# Start development mode with hot reloading
elizaos dev
# Or start in production mode
elizaos start
# Build your plugin for distribution
bun run build
The scaffolded plugin includes:
@elizaos/coreIf you prefer to create a plugin manually or need custom configuration:
mkdir plugin-my-custom
cd plugin-my-custom
bun init
# Core dependency
bun add @elizaos/core
# Development dependencies
bun add -d typescript @types/node
Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"rootDir": "./src",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Create build.ts:
#!/usr/bin/env bun
/**
* Build script for your plugin
* Uses Bun.build for bundling
*/
import { existsSync } from "node:fs";
import { rm } from "node:fs/promises";
const externalDeps = ["@elizaos/core"];
async function buildPlugin() {
console.log("🔨 Building plugin...\n");
// Clean dist directory
if (existsSync("dist")) {
await rm("dist", { recursive: true, force: true });
}
// Build with Bun
console.log("📦 Bundling with Bun...");
const buildResult = await Bun.build({
entrypoints: ["src/index.ts"],
outdir: "dist",
target: "node",
format: "esm",
sourcemap: "external",
minify: false,
external: externalDeps,
});
if (!buildResult.success) {
console.error("Build failed:");
for (const log of buildResult.logs) {
console.error(log);
}
process.exit(1);
}
console.log(`✅ Built ${buildResult.outputs.length} file(s)`);
// Generate type declarations with tsc
console.log("📝 Generating type declarations...");
const tscProcess = Bun.spawn(["bunx", "tsc", "-p", "tsconfig.build.json"], {
stdout: "inherit",
stderr: "inherit",
});
await tscProcess.exited;
if (tscProcess.exitCode !== 0) {
console.error("TypeScript declaration generation failed");
process.exit(1);
}
console.log("\n✅ Build complete!");
}
buildPlugin().catch((error) => {
console.error("Build failed:", error);
process.exit(1);
});
import type { Plugin } from "@elizaos/core";
import { myAction } from "./actions/myAction";
import { myProvider } from "./providers/myProvider";
import { MyService } from "./services/myService";
export const myPlugin: Plugin = {
name: "my-custom-plugin",
description: "A custom plugin for elizaOS",
actions: [myAction],
providers: [myProvider],
services: [MyService],
init: async (config, runtime) => {
console.log("Plugin initialized");
},
};
export default myPlugin;
from elizaos import Plugin, Action, Provider
from .actions.my_action import my_action
from .providers.my_provider import my_provider
my_plugin = Plugin(
name="my-custom-plugin",
description="A custom plugin for elizaOS",
actions=[my_action],
providers=[my_provider],
)
async def init(config, runtime):
print("Plugin initialized")
my_plugin.init = init
use elizaos::{Plugin, Action, Provider, IAgentRuntime};
use anyhow::Result;
mod actions;
mod providers;
pub fn create_my_plugin() -> Result<Plugin> {
Ok(Plugin {
name: "my-custom-plugin".to_string(),
description: "A custom plugin for elizaOS".to_string(),
actions: vec![actions::my_action()],
providers: vec![providers::my_provider()],
init: Some(Box::new(|_config, _runtime| {
Box::pin(async {
println!("Plugin initialized");
Ok(())
})
})),
..Default::default()
})
}
{
"name": "@myorg/plugin-custom",
"version": "0.1.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"scripts": {
"build": "bun run build.ts",
"dev": "bun --hot build.ts",
"test": "vitest"
}
}
If developing within the elizaOS monorepo:
package.json as a workspace dependency:{
"dependencies": {
"@yourorg/plugin-myplugin": "workspace:*"
}
}
Run bun install in the root directory
Use the plugin in your project:
import { myPlugin } from "@yourorg/plugin-myplugin";
const agent = {
name: "MyAgent",
plugins: [myPlugin],
};
For plugins outside the elizaOS monorepo:
# In your plugin directory
bun install
bun run build
bun link
# In your project directory
cd packages/project-starter
bun link @yourorg/plugin-myplugin
package.json:{
"dependencies": {
"@yourorg/plugin-myplugin": "link:@yourorg/plugin-myplugin"
}
}
src/
__tests__/
test-utils.ts # Shared test utilities (real runtime helpers)
index.test.ts # Main plugin tests
actions.test.ts # Action tests
providers.test.ts # Provider tests
evaluators.test.ts # Evaluator tests
services.test.ts # Service tests
actions/
providers/
evaluators/
services/
index.ts
import {
describe,
expect,
it,
vi,
beforeEach,
afterEach,
} from "vitest";
import {
type IAgentRuntime,
type Memory,
type State,
type HandlerCallback,
type Action,
type Provider,
type Evaluator,
ModelType,
logger,
} from "@elizaos/core";
Create a test-utils.ts file with test helpers. All tests use real runtime instances with PGLite:
import {
type IAgentRuntime,
type Memory,
type State,
type Character,
type UUID,
} from "@elizaos/core";
import { AgentRuntime } from "@elizaos/core";
import { v4 as uuidv4 } from "uuid";
/**
* Creates a real AgentRuntime for testing with PGLite database.
* NO MOCKS - all tests use actual runtime infrastructure.
*/
export async function createTestRuntime(
characterOverrides?: Partial<Character>,
): Promise<IAgentRuntime> {
const agentId = uuidv4() as UUID;
const character: Character = {
id: agentId,
name: "TestAgent",
bio: "A test agent",
plugins: [],
settings: {},
...characterOverrides,
};
const runtime = new AgentRuntime({
agentId,
character,
logLevel: "error",
});
await runtime.initialize();
return runtime;
}
/**
* Cleans up a test runtime after tests complete
*/
export async function cleanupRuntime(runtime: IAgentRuntime): Promise<void> {
await runtime.stop();
}
/**
* Creates a test Memory object
*/
export function createTestMemory(overrides?: Partial<Memory>): Memory {
return {
id: uuidv4() as UUID,
entityId: uuidv4() as UUID,
roomId: uuidv4() as UUID,
content: {
text: "Test message",
...overrides?.content,
},
createdAt: Date.now(),
...overrides,
} as Memory;
}
/**
* Creates a test State object
*/
export function createTestState(overrides?: Partial<State>): State {
return {
values: {
test: "value",
...overrides?.values,
},
data: overrides?.data || {},
text: overrides?.text || "Test state",
} as State;
}
All tests use real runtime instances - no mocks:
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { myAction } from "../src/actions/myAction";
import {
createTestRuntime,
cleanupRuntime,
createTestMemory,
createTestState,
} from "./test-utils";
import type { IAgentRuntime, Memory, State } from "@elizaos/core";
describe("MyAction", () => {
let runtime: IAgentRuntime;
let message: Memory;
let state: State;
beforeEach(async () => {
// Create real runtime with PGLite database
runtime = await createTestRuntime();
runtime.setSetting("MY_API_KEY", "test-key");
message = createTestMemory({ content: { text: "Do the thing" } });
state = createTestState();
});
afterEach(async () => {
await cleanupRuntime(runtime);
});
describe("validation", () => {
it("should validate when all requirements are met", async () => {
const isValid = await myAction.validate(runtime, message, state);
expect(isValid).toBe(true);
});
it("should not validate without required setting", async () => {
runtime.setSetting("MY_API_KEY", null);
const isValid = await myAction.validate(runtime, message, state);
expect(isValid).toBe(false);
});
});
describe("handler", () => {
it("should return success ActionResult on successful execution", async () => {
const callback = vi.fn();
const result = await myAction.handler(
runtime,
message,
state,
{},
callback,
);
expect(result.success).toBe(true);
expect(result.text).toContain("completed");
expect(result.values).toHaveProperty("lastActionTime");
expect(callback).toHaveBeenCalled();
});
it("should handle errors gracefully", async () => {
// Remove required setting to trigger error
runtime.setSetting("MY_API_KEY", null);
const result = await myAction.handler(runtime, message, state);
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
expect(result.text).toContain("Failed");
});
});
describe("examples", () => {
it("should have valid example structure", () => {
expect(myAction.examples).toBeDefined();
expect(Array.isArray(myAction.examples)).toBe(true);
// Each example should be a conversation array
for (const example of myAction.examples!) {
expect(Array.isArray(example)).toBe(true);
// Each message should have name and content
for (const message of example) {
expect(message).toHaveProperty("name");
expect(message).toHaveProperty("content");
}
}
});
});
});
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { myProvider } from "../src/providers/myProvider";
import {
createTestRuntime,
cleanupRuntime,
createTestMemory,
createTestState,
} from "./test-utils";
import type { IAgentRuntime, Memory, State } from "@elizaos/core";
describe("MyProvider", () => {
let runtime: IAgentRuntime;
let message: Memory;
let state: State;
beforeEach(async () => {
runtime = await createTestRuntime();
message = createTestMemory();
state = createTestState();
});
afterEach(async () => {
await cleanupRuntime(runtime);
});
it("should return provider result with text and data", async () => {
const result = await myProvider.get(runtime, message, state);
expect(result).toBeDefined();
expect(result.text).toContain("Current");
expect(result.data).toBeDefined();
expect(result.values).toBeDefined();
});
it("should handle missing data gracefully", async () => {
// Test with empty message content
message.content.text = "";
const result = await myProvider.get(runtime, message, state);
expect(result.text).toBeDefined();
});
});
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { MyService } from "../src/services/myService";
import { createTestRuntime, cleanupRuntime } from "./test-utils";
import type { IAgentRuntime } from "@elizaos/core";
describe("MyService", () => {
let runtime: IAgentRuntime;
let service: MyService;
beforeEach(async () => {
runtime = await createTestRuntime();
runtime.setSetting("MY_API_KEY", "test-api-key");
});
afterEach(async () => {
if (service) {
await service.stop();
}
await cleanupRuntime(runtime);
});
it("should initialize successfully with valid config", async () => {
service = await MyService.start(runtime);
expect(service).toBeDefined();
expect(service.capabilityDescription).toBeDefined();
});
it("should throw error without API key", async () => {
runtime.setSetting("MY_API_KEY", null);
await expect(MyService.start(runtime)).rejects.toThrow(
"MY_API_KEY not configured",
);
});
it("should clean up resources on stop", async () => {
service = await MyService.start(runtime);
await service.stop();
// Verify cleanup happened
});
});
For integration testing with a live runtime:
// tests/e2e/myPlugin.e2e.ts
export const myPluginE2ETests = {
name: "MyPlugin E2E Tests",
tests: [
{
name: "should execute full plugin flow",
fn: async (runtime: IAgentRuntime) => {
// Create test message
const message: Memory = {
id: generateId(),
entityId: "test-user",
roomId: runtime.agentId,
content: {
text: "Please do the thing",
source: "test",
},
};
// Store message
await runtime.createMemory(message, "messages");
// Compose state
const state = await runtime.composeState(message);
// Execute action
const result = await myAction.handler(
runtime,
message,
state,
{},
async (response) => {
// Verify callback responses
expect(response.text).toBeDefined();
},
);
// Verify result
expect(result.success).toBe(true);
// Verify side effects
const memories = await runtime.getMemories({
roomId: message.roomId,
tableName: "action_results",
count: 1,
});
expect(memories.length).toBeGreaterThan(0);
},
},
],
};
# Run all tests
npx vitest
# Run specific test file
npx vitest src/__tests__/actions.test.ts
# Run with watch mode
npx vitest --watch
# Run with coverage
npx vitest --coverage
AgentRuntime with PGLite - no mocksbeforeEach/afterEach to create and cleanup runtime instances# Watch mode with hot reloading
bun run dev
# Or with elizaOS CLI
elizaos dev
# Build the plugin
bun run build
# Output will be in dist/
# Login to npm
npm login
# Publish
npm publish --access public
Update package.json:
{
"name": "@yourorg/plugin-name",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
}
}
Then publish:
npm publish
# Bump version
npm version patch # 0.1.0 -> 0.1.1
npm version minor # 0.1.0 -> 0.2.0
npm version major # 0.1.0 -> 1.0.0
import { logger } from "@elizaos/core";
// In your plugin
logger.debug("Plugin initialized", { config });
logger.info("Action executed", { result });
logger.error("Failed to connect", { error });
Create .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Plugin",
"runtimeExecutable": "bun",
"program": "${workspaceFolder}/src/index.ts",
"cwd": "${workspaceFolder}",
"console": "integratedTerminal"
}
]
}
Solution: Check that your plugin is properly exported and added to the agent's plugin array.
Solution: Ensure @elizaos/core is installed and TypeScript is configured correctly.
Solution: Verify the service is registered in the plugin and started properly.
Solution: Make sure your tsconfig.json has proper module resolution settings for Bun.