--- title: "Create a Plugin" sidebarTitle: "Create a Plugin" description: "Step-by-step tutorial for building a Eliza plugin from scratch — scaffolding, actions, providers, testing, and local development." --- This tutorial walks you through creating a complete plugin from scratch. By the end you will have a working plugin with an action, a provider, and a background service running inside the Eliza runtime. ## Prerequisites - Node.js 22 or later - A working Eliza installation (`eliza start` runs without errors) ## Step 1: Scaffold the Project Create the directory structure: ``` my-plugin/ ├── package.json ├── tsconfig.json └── src/ └── index.ts ``` ### package.json ```json { "name": "@elizaos/plugin-my-feature", "version": "1.0.0", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "build": "tsc", "dev": "tsc --watch", "test": "vitest run" }, "dependencies": { "@elizaos/core": "^2.0.0" }, "devDependencies": { "typescript": "^5.0.0", "vitest": "^4.0.0" } } ``` ### tsconfig.json ```json { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "outDir": "dist", "declaration": true, "strict": true }, "include": ["src"] } ``` ## Step 2: Implement an Action Actions are things the agent can do. The LLM selects actions from the registered list based on description and examples. ```typescript // src/actions/weather.ts import type { Action } from "@elizaos/core"; export const checkWeatherAction: Action = { name: "CHECK_WEATHER", description: "Check the current weather for a city", similes: ["GET_WEATHER", "WEATHER_LOOKUP", "FORECAST"], validate: async (_runtime, _message, _state) => { // Return false if the required API key is missing return Boolean(process.env.WEATHER_API_KEY); }, handler: async (_runtime, _message, _state, options, _callback) => { const params = options?.parameters as Record | undefined; const city = typeof params?.city === "string" ? params.city : "London"; try { const url = `https://api.example-weather.com/current?city=${encodeURIComponent(city)}&key=${process.env.WEATHER_API_KEY}`; const res = await fetch(url); const data = await res.json() as { temp: number; condition: string }; return { success: true, text: `Weather in ${city}: ${data.temp}°C, ${data.condition}`, data: { city, temp: data.temp, condition: data.condition }, }; } catch (err) { return { success: false, error: `Failed to fetch weather: ${err instanceof Error ? err.message : String(err)}`, }; } }, parameters: [ { name: "city", description: "The city to check weather for", required: false, schema: { type: "string" }, }, ], examples: [ [ { user: "user", content: { text: "What's the weather in Tokyo?" } }, { user: "assistant", content: { text: "Weather in Tokyo: 22°C, Partly cloudy", action: "CHECK_WEATHER" } }, ], ], }; ``` ## Step 3: Implement a Provider Providers inject context into the agent's prompt before each LLM inference. Unlike actions, they run automatically. ```typescript // src/providers/status.ts import type { Provider } from "@elizaos/core"; export const pluginStatusProvider: Provider = { name: "weatherPluginStatus", description: "Provides current plugin status and configuration", position: 10, // Run after core providers get: async (_runtime, _message, _state) => { const hasApiKey = Boolean(process.env.WEATHER_API_KEY); return { text: hasApiKey ? "Weather plugin is active. You can check weather for any city." : "Weather plugin is configured but missing WEATHER_API_KEY.", values: { weatherPluginActive: hasApiKey, }, }; }, }; ``` ## Step 4: Implement a Service Services are long-running background processes that start with the runtime. ```typescript // src/services/weather-cache.ts import type { IAgentRuntime, Service } from "@elizaos/core"; let cacheInterval: NodeJS.Timeout | undefined; const weatherCache = new Map(); export const WeatherCacheService = { serviceType: "weather_cache", start: async (_runtime: IAgentRuntime): Promise => { // Refresh cache every 10 minutes cacheInterval = setInterval(() => { const now = Date.now(); for (const [city, entry] of weatherCache) { if (now - entry.fetchedAt > 10 * 60 * 1000) { weatherCache.delete(city); } } }, 60_000); return { stop: async () => { if (cacheInterval) clearInterval(cacheInterval); weatherCache.clear(); }, } as Service; }, }; ``` ## Step 5: Assemble the Plugin ```typescript // src/index.ts import type { Plugin } from "@elizaos/core"; import { checkWeatherAction } from "./actions/weather"; import { pluginStatusProvider } from "./providers/status"; import { WeatherCacheService } from "./services/weather-cache"; const weatherPlugin: Plugin = { name: "weather-plugin", description: "Provides real-time weather information for any city", priority: 10, init: async (_config, runtime) => { runtime.logger?.info("[weather-plugin] Initialized"); if (!process.env.WEATHER_API_KEY) { runtime.logger?.warn("[weather-plugin] WEATHER_API_KEY not set — CHECK_WEATHER action will be disabled"); } }, actions: [checkWeatherAction], providers: [pluginStatusProvider], services: [WeatherCacheService], }; export default weatherPlugin; ``` This is a minimal plugin. The `Plugin` interface also supports `evaluators`, `routes`, `events`, `models`, `componentTypes`, and `tests`. See [Plugin Schemas](/plugins/schemas) for all available extension points. ## Step 6: Write Tests ```typescript // src/index.test.ts import { describe, it, expect } from "vitest"; import { AgentRuntime, createCharacter, createMessageMemory, InMemoryDatabaseAdapter, stringToUuid, type IAgentRuntime, type Memory, } from "@elizaos/core"; import weatherPlugin from "./index"; function createRuntime(): IAgentRuntime { return new AgentRuntime({ agentId: stringToUuid("weather-test-agent"), character: createCharacter({ name: "Weather Test" }), adapter: new InMemoryDatabaseAdapter(), plugins: [], logLevel: "error", }); } const mockMessage: Memory = createMessageMemory({ entityId: stringToUuid("weather-test-user"), roomId: stringToUuid("weather-test-room"), content: { text: "What is the weather in Paris?" }, }); describe("weather-plugin", () => { it("exports a valid plugin", () => { expect(weatherPlugin.name).toBe("weather-plugin"); expect(weatherPlugin.actions).toHaveLength(1); expect(weatherPlugin.providers).toHaveLength(1); }); it("CHECK_WEATHER action fails validation without API key", async () => { delete process.env.WEATHER_API_KEY; const action = weatherPlugin.actions![0]; const valid = await action.validate(createRuntime(), mockMessage); expect(valid).toBe(false); }); it("CHECK_WEATHER action passes validation with API key", async () => { process.env.WEATHER_API_KEY = "test-key"; const action = weatherPlugin.actions![0]; const valid = await action.validate(createRuntime(), mockMessage); expect(valid).toBe(true); delete process.env.WEATHER_API_KEY; }); }); ``` ## Step 7: Register with Runtime | Option | Best for | How it works | |--------|----------|--------------| | **A: Local Plugin** | Active development and testing | Auto-discovered from the project's `plugins/` directory | | **B: Config-Based** | Persistent installations with explicit control | Referenced by path in `eliza.json` | | **C: Character File** | Per-agent plugin sets | Listed in the character definition, loaded at agent start | ### Option A: Local Plugin (Development) Place the plugin directory inside the project: ``` eliza-project/ └── plugins/ └── weather-plugin/ ├── package.json └── src/index.ts ``` Eliza automatically discovers plugins in the `plugins/` directory. ### Option B: Config-Based Loading Add to `eliza.json`: ```json { "plugins": { "allow": ["weather-plugin"], "entries": { "weather-plugin": { "path": "./plugins/weather-plugin" } } } } ``` ### Option C: Character File ```json { "name": "MyAgent", "plugins": ["./plugins/weather-plugin"], "settings": { "secrets": { "WEATHER_API_KEY": "your-key-here" } } } ``` ## Step 8: Build and Test ```bash # Build the plugin cd my-plugin && bun run build # Run tests bun test # Start Eliza with the plugin loaded eliza start ``` Check the logs for `[weather-plugin] Initialized` to confirm the plugin loaded. ## Plugin Manifest (`elizaos.plugin.json`) Every published plugin should include an `elizaos.plugin.json` manifest at its package root. This file tells the runtime and admin UI how to configure and display your plugin. ```json { "id": "plugin-weather", "name": "Weather Plugin", "version": "1.0.0", "kind": "feature", "description": "Provides real-time weather data to your agent", "configSchema": { "type": "object", "properties": { "apiKey": { "type": "string", "description": "OpenWeatherMap API key" }, "units": { "type": "string", "enum": ["metric", "imperial"], "default": "metric" } }, "required": ["apiKey"] }, "uiHints": { "apiKey": { "label": "API Key", "type": "password", "help": "Get one at openweathermap.org/appid" }, "units": { "label": "Temperature Units", "type": "select", "advanced": false } }, "requiredSecrets": ["WEATHER_API_KEY"], "channels": ["chat", "telegram", "discord"], "dependencies": ["knowledge"] } ``` ### Manifest Fields | Field | Type | Description | |-------|------|-------------| | `id` | `string` | Unique plugin identifier (kebab-case) | | `name` | `string` | Human-readable display name | | `version` | `string` | Semver version | | `kind` | `PluginKind` | One of: `feature`, `ai-provider`, `connector`, `database`, `app`, `memory`, `channel`, `provider`, `skill` | | `configSchema` | `JsonSchema` | JSON Schema for plugin configuration | | `uiHints` | `Record` | Hints for admin panel rendering, keyed by config property name | | `requiredSecrets` | `string[]` | Environment variables that must be set | | `channels` | `string[]` | Supported communication channels | | `dependencies` | `string[]` | Other plugins this depends on | Additional fields like `optionalSecrets`, `providers`, `skills`, `gatewayMethods`, and `cliCommands` are also supported. See [Plugin Schemas](/plugins/schemas) for the complete manifest reference. ### UI Hints The `uiHints` object controls how config fields appear in the admin dashboard. Each key matches a property name in `configSchema`: ```typescript interface PluginConfigUiHint { label: string; // display label type: 'text' | 'password' | 'number' | 'select' | 'toggle' | 'textarea'; help?: string; // tooltip or helper text sensitive?: boolean; // if true, value is masked in the UI advanced?: boolean; // if true, hidden under "Advanced" toggle } ``` --- ## How Plugin Discovery Works When Eliza starts, it discovers plugins from multiple sources in this order: 1. **Eliza plugin** — Built-in workspace context and session management 2. **Core plugins** — Always loaded (`@elizaos/plugin-sql`, `@elizaos/plugin-local-inference`, etc.) 3. **Connector plugins** — Auto-enabled when channel config exists (e.g., `telegram` config → `@elizaos/plugin-telegram`) 4. **Provider plugins** — Auto-enabled when API key env var is set (e.g., `ANTHROPIC_API_KEY` → `@elizaos/plugin-anthropic`) 5. **Feature plugins** — Enabled via feature flags in `eliza.json` (e.g., `features.browser: true` → `@elizaos/plugin-browser`) 6. **Ejected plugins** — Git-cloned upstream plugins in `~/.local/state/eliza/plugins/ejected/` (take priority over npm versions) 7. **User-installed plugins** — Installed via `bun add ` (after editing character.plugins) 8. **Custom plugins** — Dropped into `~/.local/state/eliza/plugins/custom/` ### Auto-Enable by Environment Variable Set an API key and the corresponding plugin loads automatically: | Environment Variable | Plugin | |---------------------|--------| | `ANTHROPIC_API_KEY` | `@elizaos/plugin-anthropic` | | `OPENAI_API_KEY` | `@elizaos/plugin-openai` | | `GOOGLE_GENERATIVE_AI_API_KEY` | `@elizaos/plugin-google-genai` | | `GROQ_API_KEY` | `@elizaos/plugin-groq` | | `OPENROUTER_API_KEY` | `@elizaos/plugin-openrouter` | ### Auto-Enable by Connector Config Configure a channel in `eliza.json` and the connector plugin loads: ```json { "connectors": { "telegram": { "botToken": "..." }, "discord": { "token": "..." } } } ``` This auto-loads `@elizaos/plugin-telegram` and `@elizaos/plugin-discord`. ### Disabling Auto-Enabled Plugins Override in `eliza.json`: ```json { "plugins": { "entries": { "telegram": { "enabled": false } } } } ``` --- ## Starter Template If you have the upstream `elizaos` CLI installed globally, you can scaffold a plugin project: ```bash # Requires the elizaos CLI (npm i -g elizaos@beta) npx elizaos@beta create my-plugin --template plugin --language typescript ``` Alternatively, copy the manual scaffold from [Step 1](#step-1-scaffold-the-project) above — it produces the same structure. The template includes: - Pre-configured `package.json` with `@elizaos/core` peer dependency - TypeScript config targeting ES2022 - Example action, provider, and service - Vitest test setup with runtime mocks - `elizaos.plugin.json` manifest --- ## Next Steps - [Testing Guide](/plugins/testing) — Unit, integration, and E2E testing patterns - [Decision Guide](/plugins/decision-guide) — Choosing between Actions, Providers, Services, and Skills - [Plugin Patterns](/plugins/patterns) — Common patterns for services, state, and error handling - [Plugin Schemas](/plugins/schemas) — Full schema reference for all plugin types - [Publish a Plugin](/plugins/publish) — Publish to the npm registry