---
title: "Development"
description: "Build plugins for elizaOS in TypeScript, Python, or Rust"
---

## Your Plugin in 3 Steps

<Tabs>
  <Tab title="TypeScript">
    1. **Scaffold** - `elizaos create my-plugin --template plugin` 2.
    **Build** - Add actions, providers, or services 3. **Test** - `bun run
    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 --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

<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:*"
  }
}
```

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"
  }
}
```

<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

# 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

<CardGroup cols={2}>
  <Card title="Plugin Components" icon="cube" href="/plugins/architecture">
    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/schemas">
  Complete API reference for all interfaces
</Card>

<Card title="Publish a Plugin" icon="upload" href="/plugins/publish">
  Share your plugin with the community
</Card>

  <Card title="Deploy to Cloud" icon="cloud" href="/eliza-cloud-deployment">
    Ship your agent with plugins to production
  </Card>
</CardGroup>
