---
title: "Development"
description: "Build plugins for elizaOS in TypeScript, Python, or Rust"
---
## Your Plugin in 3 Steps
1. **Scaffold** - `elizaos create my-plugin --template plugin` 2.
**Build** - Add actions, providers, or services 3. **Test** - `bun run
test`
1. **Create** - `mkdir plugin-my-plugin && cd plugin-my-plugin` 2. **Build**
- Define your plugin with actions and providers 3. **Test** - `pytest`
1. **Create** - `cargo new plugin-my-plugin --lib` 2. **Build** - Implement
the Plugin trait 3. **Test** - `cargo test`
That's it. No complex setup, no boilerplate to maintain.
**30 minutes to your first plugin.** Focus on your logic, not infrastructure.
elizaOS plugins work identically across TypeScript, Python, and Rust.
## Quick Start: Scaffolding Plugins with CLI
The easiest way to create a new plugin is using the elizaOS CLI, which provides interactive scaffolding with pre-configured templates.
### Using `elizaos create`
The CLI offers two plugin templates to get you started quickly:
```bash
# Interactive plugin creation
elizaos create
# Or specify the name directly
elizaos create my-plugin --template plugin
```
When creating a plugin, you'll be prompted to choose between:
1. **Quick Plugin (Backend Only)** - Simple backend-only plugin without frontend
- Perfect for: API integrations, blockchain actions, data providers
- Includes: Basic plugin structure, actions, providers, services
- No frontend components or UI routes
2. **Full Plugin (with Frontend)** - Complete plugin with React frontend and API routes
- Perfect for: Plugins that need web UI, dashboards, or visual components
- Includes: Everything from Quick Plugin + React frontend, Vite setup, API routes
- Tailwind CSS pre-configured for styling
### Quick Plugin Structure
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
```
### Full Plugin Structure
Selecting "Full Plugin" adds frontend capabilities:
```text
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]
```
### After Scaffolding
Once your plugin is created:
```bash
# Navigate to your plugin
cd plugin-my-plugin
# Install dependencies (automatically done by CLI)
bun install
# Build your plugin for distribution
bun run build
# Run tests
bun run test
```
The scaffolded plugin includes:
- ✅ Proper TypeScript configuration
- ✅ Build setup with Bun.build (and Vite for full plugins)
- ✅ Example action and provider to extend
- ✅ Integration with `@elizaos/core`
- ✅ Development scripts ready to use
- ✅ Basic tests structure
The CLI templates follow all elizaOS conventions and best practices, making it
easy to get started without worrying about configuration.
## Manual Plugin Creation
If you prefer to create a plugin manually or need custom configuration:
### 1. Initialize the Project
```bash
mkdir plugin-my-custom
cd plugin-my-custom
bun init
```
### 2. Install Dependencies
```bash
# Core dependency
bun add @elizaos/core
# Development dependencies
bun add -d typescript @types/node
```
### 3. Configure TypeScript
Create `tsconfig.json`:
```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"]
}
```
### 4. Configure Build
Create `build.ts`:
```typescript
#!/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);
});
```
### 5. Create Plugin Structure
Create `src/index.ts`:
```typescript
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;
```
Create `plugin.py`:
```python
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
```
Create `src/lib.rs`:
```rust
use elizaos::{Plugin, Action, Provider, IAgentRuntime};
use anyhow::Result;
mod actions;
mod providers;
pub fn create_my_plugin() -> Result {
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()
})
}
```
### 6. Update package.json
```json
{
"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"
}
}
```
## Using Your Plugin in Projects
### Option 1: Plugin Inside the Monorepo
If developing within the elizaOS monorepo:
1. Add your plugin to the root `package.json` as a workspace dependency:
```json
{
"dependencies": {
"@yourorg/plugin-myplugin": "workspace:*"
}
}
```
2. Run `bun install` in the root directory
3. Use the plugin in your project:
```typescript
import { myPlugin } from "@yourorg/plugin-myplugin";
const agent = {
name: "MyAgent",
plugins: [myPlugin],
};
```
### Option 2: Plugin Outside the Monorepo
For plugins outside the elizaOS monorepo:
1. In your plugin directory, build and link it:
```bash
# In your plugin directory
bun install
bun run build
bun link
```
2. In your project directory, link the plugin:
```bash
# In your project directory
cd packages/project-starter
bun link @yourorg/plugin-myplugin
```
3. Add to your project's `package.json`:
```json
{
"dependencies": {
"@yourorg/plugin-myplugin": "link:@yourorg/plugin-myplugin"
}
}
```
When using `bun link`, remember to rebuild your plugin (`bun run build`) after
making changes for them to be reflected in your project.
## Testing Plugins
### Test Environment Setup
#### Directory Structure
```
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
```
#### Base Test Imports
```typescript
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";
```
### Creating Test Utilities
Create a `test-utils.ts` file with test helpers. All tests use **real runtime instances** with PGLite:
```typescript
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,
): Promise {
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 {
await runtime.stop();
}
/**
* Creates a test Memory object
*/
export function createTestMemory(overrides?: Partial): 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 {
return {
values: {
test: "value",
...overrides?.values,
},
data: overrides?.data || {},
text: overrides?.text || "Test state",
} as State;
}
```
### Testing Actions
All tests use **real runtime instances** - no mocks:
```typescript
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");
}
}
});
});
});
```
### Testing Providers
```typescript
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();
});
});
```
### Testing Services
```typescript
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
});
});
```
### E2E Testing
For integration testing with a live runtime:
```typescript
// 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);
},
},
],
};
```
### Running Tests
```bash
# 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
```
### Test Best Practices
1. **Use Real Runtime**: All tests use actual `AgentRuntime` with PGLite - no mocks
2. **Isolate Tests**: Use `beforeEach`/`afterEach` to create and cleanup runtime instances
3. **Test Happy Path and Errors**: Cover both success and failure cases
4. **Test Validation Logic**: Ensure actions validate correctly
5. **Test Examples**: Verify example structures are valid
6. **Test Side Effects**: Verify database writes with real database operations
7. **Use Descriptive Names**: Make test purposes clear
8. **Keep Tests Fast**: PGLite provides fast in-memory database
## Development Workflow
### 1. Development Mode
```bash
# Watch mode with hot reloading
bun run dev
# Run the generated development script when present
bun run dev
```
### 2. Building for Production
```bash
# Build the plugin
bun run build
# Output will be in dist/
```
### 3. Publishing
#### To npm
```bash
# Login to npm
npm login
# Publish
npm publish --access public
```
#### To GitHub Packages
Update `package.json`:
```json
{
"name": "@yourorg/plugin-name",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
}
}
```
Then publish:
```bash
npm publish
```
### 4. Version Management
```bash
# 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
```
## Debugging
### Enable Debug Logging
```typescript
import { logger } from "@elizaos/core";
// In your plugin
logger.debug("Plugin initialized", { config });
logger.info("Action executed", { result });
logger.error("Failed to connect", { error });
```
### VS Code Debug Configuration
Create `.vscode/launch.json`:
```json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Plugin",
"runtimeExecutable": "bun",
"program": "${workspaceFolder}/src/index.ts",
"cwd": "${workspaceFolder}",
"console": "integratedTerminal"
}
]
}
```
## Common Issues and Solutions
### Issue: Plugin not loading
**Solution**: Check that your plugin is properly exported and added to the agent's plugin array.
### Issue: TypeScript errors
**Solution**: Ensure `@elizaos/core` is installed and TypeScript is configured correctly.
### Issue: Service not available
**Solution**: Verify the service is registered in the plugin and started properly.
### Issue: Tests failing with module errors
**Solution**: Make sure your `tsconfig.json` has proper module resolution settings for Bun.
## See Also
Deep dive into Actions, Providers, Evaluators, and Services
Learn proven plugin development patterns
Understand plugin configuration and validation
Complete API reference for all interfaces
Share your plugin with the community
Ship your agent with plugins to production