Back to Eliza

Development

packages/docs/plugins/development.mdx

1.7.221.5 KB
Original Source

Your Plugin in 3 Steps

<Tabs> <Tab title="TypeScript"> 1. **Scaffold** - `elizaos create my-plugin --type plugin` 2. **Build** - Add actions, providers, or services 3. **Test** - `elizaos test` </Tab> <Tab title="Python"> 1. **Create** - `mkdir plugin-my-plugin && cd plugin-my-plugin` 2. **Build** - Define your plugin with actions and providers 3. **Test** - `pytest` </Tab> <Tab title="Rust"> 1. **Create** - `cargo new plugin-my-plugin --lib` 2. **Build** - Implement the Plugin trait 3. **Test** - `cargo test` </Tab> </Tabs>

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>

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

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

  • ✅ 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
<Tip> The CLI templates follow all elizaOS conventions and best practices, making it easy to get started without worrying about configuration. </Tip>

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

<Tabs> <Tab title="TypeScript"> 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;
</Tab> <Tab title="Python"> 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
</Tab> <Tab title="Rust"> Create `src/lib.rs`:
rust
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()
    })
}
</Tab> </Tabs>

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:*"
  }
}
  1. Run bun install in the root directory

  2. 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
  1. In your project directory, link the plugin:
bash
# In your project directory
cd packages/project-starter
bun link @yourorg/plugin-myplugin
  1. Add to your project's package.json:
json
{
  "dependencies": {
    "@yourorg/plugin-myplugin": "link:@yourorg/plugin-myplugin"
  }
}
<Note> When using `bun link`, remember to rebuild your plugin (`bun run build`) after making changes for them to be reflected in your project. </Note>

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

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

# Or with elizaOS CLI
elizaos 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

<CardGroup cols={2}> <Card title="Plugin Components" icon="cube" href="/plugins/components"> Deep dive into Actions, Providers, Evaluators, and Services </Card> <Card title="Common Patterns" icon="lightbulb" href="/plugins/patterns"> Learn proven plugin development patterns </Card> <Card title="Plugin Schemas" icon="list" href="/plugins/schemas"> Understand plugin configuration and validation </Card> <Card title="Plugin Reference" icon="book" href="/plugins/reference"> Complete API reference for all interfaces </Card> <Card title="Publish a Plugin" icon="upload" href="/guides/publish-a-plugin"> Share your plugin with the community </Card> <Card title="Deploy to Cloud" icon="cloud" href="/guides/deploy-to-cloud"> Ship your agent with plugins to production </Card> </CardGroup>