--- 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