packages/docs/plugins/migration.mdx
Important: This comprehensive guide will walk you through migrating your elizaOS plugins from version 0.x to 1.x. The migration process involves several key changes to architecture, APIs, and best practices.
The 1.x architecture brings:
Before starting your migration:
Create a new branch for the 1.x version while preserving the main branch for backwards compatibility:
git checkout -b 1.x
Note: This branch will serve as your new 1.x version branch.
Clean up deprecated tooling and configuration files:
biome.json - Deprecated linter configurationvitest.config.ts - Replaced by Bun test runnerlock.json or yml.lock filesrm -rf vitest.config.ts
rm -rf biome.json
rm -f *.lock.json *.yml.lock
Why? The elizaOS ecosystem has standardized on:
- Bun's built-in test runner (replacing Vitest)
- Prettier for code formatting (replacing Biome)
{
"version": "1.0.0",
"name": "@elizaos/plugin-yourname" // Note: @elizaos, not @elizaos-plugins
}
{
"dependencies": {
"@elizaos/core": "^2.0.0"
},
"devDependencies": {
"prettier": "^3.0.0",
"bun": "^1.2.15", // REQUIRED
"@types/bun": "latest", // REQUIRED
"typescript": "^5.0.0"
}
}
"scripts": {
"build": "bun run build.ts",
"dev": "bun --hot build.ts",
"lint": "prettier --write ./src",
"clean": "rm -rf dist .turbo node_modules",
"test": "vitest",
"publish": "npm publish --access public"
}
Update tsconfig.json:
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"moduleResolution": "bundler",
"outDir": "dist",
"rootDir": "src",
"types": ["bun-types"],
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "__tests__", "**/*.test.ts"]
}
// OLD (0.x)
import { Action, Memory, State } from "@ai16z/eliza";
// NEW (1.x)
import { Action, Memory, State, ActionResult } from "@elizaos/core";
Actions in 1.x must return ActionResult and include callbacks:
// OLD (0.x)
const myAction = {
handler: async (runtime, message, state) => {
return { text: "Response" };
},
};
// NEW (1.x)
const myAction = {
handler: async (runtime, message, state, options, callback) => {
// Use callback for intermediate responses
await callback?.({ text: "Processing..." });
// Must return ActionResult
return {
success: true, // REQUIRED field
text: "Response completed",
values: {
/* state updates */
},
data: {
/* raw data */
},
};
},
};
// Error handling
handler: async (runtime, message, state, options, callback) => {
try {
const result = await performAction();
return {
success: true,
text: `Action completed: ${result}`,
data: result,
};
} catch (error) {
return {
success: false,
text: "Action failed",
error: error instanceof Error ? error : new Error(String(error)),
};
}
};
// Using previous results (action chaining)
handler: async (runtime, message, state, options, callback) => {
const context = options?.context;
const previousResult = context?.getPreviousResult?.("PREVIOUS_ACTION");
if (previousResult?.data) {
// Use data from previous action
const continuedResult = await continueWork(previousResult.data);
return {
success: true,
text: "Continued from previous action",
data: continuedResult,
};
}
};
The v1 composeState method has enhanced filtering capabilities:
// v0: Basic state composition
const state = await runtime.composeState(message);
// v1: With filtering
const state = await runtime.composeState(
message,
["agentName", "bio", "recentMessages"], // Include only these
true, // onlyInclude = true
);
// v1: Update specific parts
const updatedState = await runtime.composeState(
message,
["RECENT_MESSAGES", "GOALS"], // Update only these
);
Core state keys you can filter:
agentId, agentName, bio, lore, adjectiverecentMessages, recentMessagesDataTIME, FACTS)// v0: Direct state access
const data = await runtime.databaseAdapter.getData();
// v1: Provider pattern
const provider: Provider = {
name: "MY_DATA",
description: "Provides custom data",
dynamic: true, // Re-fetch on each use
get: async (runtime, message, state) => {
const data = await runtime.databaseAdapter.getData();
return {
text: formatDataForPrompt(data),
values: data,
data: { raw: data },
};
},
};
interface Provider {
name: string;
description?: string;
dynamic?: boolean; // Default: false
position?: number; // Execution order (-100 to 100)
private?: boolean; // Hidden from provider list
get: (runtime, message, state) => Promise<ProviderResult>;
}
All tests use real runtime instances with PGLite - no mocks:
// Using Vitest for tests with real runtime
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { createTestRuntime, cleanupRuntime } from "./test-utils";
import type { IAgentRuntime } from "@elizaos/core";
let runtime: IAgentRuntime;
beforeEach(async () => {
runtime = await createTestRuntime();
});
afterEach(async () => {
await cleanupRuntime(runtime);
});
Run tests with:
npx vitest
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { myAction } from "../src/actions/myAction";
import { createTestRuntime, cleanupRuntime } from "./test-utils";
import type { IAgentRuntime, Memory } from "@elizaos/core";
describe("MyAction", () => {
let runtime: IAgentRuntime;
beforeEach(async () => {
// Create real runtime with PGLite database
runtime = await createTestRuntime();
});
afterEach(async () => {
await cleanupRuntime(runtime);
});
it("should validate correctly", async () => {
const message = { content: { text: "test" } } as Memory;
const isValid = await myAction.validate(runtime, message);
expect(isValid).toBe(true);
});
it("should return ActionResult", async () => {
const message = { content: { text: "test" } } as Memory;
const result = await myAction.handler(runtime, message);
expect(result).toHaveProperty("success");
expect(result.success).toBe(true);
});
});
// v0: Simple template
const template = `{{agentName}} responds to {{userName}}`;
// v1: Enhanced templates with conditional blocks
const template = `
{{#if hasGoals}}
Current goals: {{goals}}
{{/if}}
{{agentName}} considers the context and responds.
`;
import { composePromptFromState } from "@elizaos/core";
const prompt = composePromptFromState({
state,
template: myTemplate,
additionalContext: {
customField: "value",
},
});
const response = await runtime.useModel(ModelType.TEXT_LARGE, {
prompt,
runtime,
});
// v1: Service pattern
export class MyService extends Service {
static serviceType = "my-service";
capabilityDescription = "My service capabilities";
static async start(runtime: IAgentRuntime): Promise<MyService> {
const service = new MyService(runtime);
await service.initialize();
return service;
}
async stop(): Promise<void> {
// Cleanup
}
}
// v1: Event system
export const myPlugin: Plugin = {
name: "my-plugin",
events: {
MESSAGE_RECEIVED: [
async (params) => {
// Handle message received event
},
],
RUN_COMPLETED: [
async (params) => {
// Handle run completed event
},
],
},
};
dist
node_modules
.env
.elizadb
.turbo
*
!dist/**
!package.json
!readme.md
!build.ts
Create a LICENSE file with MIT license text.
Ensure all required fields are present:
name, version, descriptionmain, types, moduleauthor, license, repositoryscripts, dependencies, devDependenciestype: "module"exports configurationSolution: Ensure all actions return ActionResult with success field:
return {
success: true, // REQUIRED
text: "Response",
values: {},
data: {},
};
Solution: Update imports to use vitest:
import { describe, it, expect, vi } from "vitest";
Solution: Use filtering to only compose needed state:
const state = await runtime.composeState(
message,
["RECENT_MESSAGES", "CHARACTER"],
true, // onlyInclude
);
Solution: Ensure provider is registered and not marked as private:
const provider = {
name: "MY_PROVIDER",
private: false, // Make sure it's not private
dynamic: false, // Static providers are included by default
get: async () => {
/* ... */
},
};
bun run build
npx vitest
Create a test agent using your migrated plugin:
import { myPlugin } from "./dist/index.js";
const agent = {
name: "TestAgent",
plugins: [myPlugin],
};
// Test your plugin functionality
Once migration is complete:
# Build the plugin
bun run build
# Test everything
npx vitest
# Publish to npm
npm publish --access public
@elizaos/coreActionResult with success fieldIf you encounter issues during migration:
Evaluators remain largely unchanged in their core structure, but their integration with the runtime has evolved:
// v0 Evaluator usage remains the same
export interface Evaluator {
alwaysRun?: boolean;
description: string;
similes: string[];
examples: EvaluationExample[];
handler: Handler;
name: string;
validate: Validator;
}
evaluate() method now returns Evaluator[] instead of string[]:// v0: Returns string array of evaluator names
const evaluators: string[] = await runtime.evaluate(message, state);
// v1: Returns Evaluator objects
const evaluators: Evaluator[] | null = await runtime.evaluate(message, state);
// v1: Extended evaluate signature
await runtime.evaluate(
message: Memory,
state?: State,
didRespond?: boolean,
callback?: HandlerCallback,
responses?: Memory[] // NEW: Can pass responses for evaluation
);
The most significant change is the shift from User/Participant to Entity/Room/World:
// v0: User-based methods
await runtime.ensureUserExists(userId, userName, name, email, source);
const account = await runtime.getAccountById(userId);
// v1: Entity-based methods
await runtime.ensureConnection({
entityId: userId,
roomId,
userName,
name,
worldId,
source,
});
const entity = await runtime.getEntityById(entityId);
// v0: Participant methods
await runtime.ensureParticipantExists(userId, roomId);
await runtime.ensureParticipantInRoom(userId, roomId);
// v1: Simplified room membership
await runtime.ensureParticipantInRoom(entityId, roomId);
v1 introduces the concept of "worlds" (servers/environments):
// v1: World management
await runtime.ensureWorldExists({
id: worldId,
name: serverName,
type: "discord", // or other platform
});
// Get all rooms in a world
const rooms = await runtime.getRooms(worldId);
// v0: Multiple ensure methods
await runtime.ensureUserExists(...);
await runtime.ensureRoomExists(roomId);
await runtime.ensureParticipantInRoom(...);
// v1: Single connection method
await runtime.ensureConnection({
entityId,
roomId,
worldId,
userName,
name,
source,
channelId,
serverId,
type: 'user',
metadata: {}
});
Clients now have a simpler interface:
// v0: Client with config
export type Client = {
name: string;
config?: { [key: string]: string | number | boolean };
start: (runtime: IAgentRuntime) => Promise<ClientInstance>;
};
// v1: Client integrated with services
// Clients are now typically implemented as services
class MyClient extends Service {
static serviceType = ServiceTypeName.MY_CLIENT;
async initialize(runtime: IAgentRuntime): Promise<void> {
// Start client operations
}
async stop(): Promise<void> {
// Stop client operations
}
}
updateRecentMessageState() → Use composeState(message, ['RECENT_MESSAGES'])registerMemoryManager() → Not needed, use database adaptergetMemoryManager() → Use database adapter methodsregisterContextProvider() → Use registerProvider()evaluate() → Now returns Evaluator[] instead of string[]getAccountById() → getEntityById()ensureUserExists() → ensureConnection()generateText() → runtime.useModel()setSetting()registerEvent()emitEvent()useModel()registerModel()ensureWorldExists()getRooms()Evaluator[] objectsupdateRecentMessageState with composeStategenerateText to runtime.useModelimport { settings } with runtime.getSetting() callsruntime parameter where settings are neededThe migration from 0.x to 1.x involves:
ActionResultTake your time, test thoroughly, and don't hesitate to ask for help in the community!