Actions
Complete guide to implementing actions in ElizaOS, including the Action interface, handler patterns, validation, and real-world examples from plugin-bootstrap
Actions are the interactive behaviors that agents can perform in ElizaOS. They define how agents respond to different situations and execute specific tasks. This page covers the Action interface, implementation patterns, and practical examples from the plugin-bootstrap package.
Action Interface
The Action interface defines the structure for all agent actions:
interface Action {
/** Similar action descriptions for context */
similes?: string[];
/** Detailed description of what the action does */
description: string;
/** Example usage scenarios */
examples?: ActionExample[][];
/** Core handler function that executes the action */
handler: Handler;
/** Action name/identifier */
name: string;
/** Validation function to determine if action should execute */
validate: Validator;
}
Core Components
Handler Function
The handler is the core execution logic:
type Handler = (
runtime: IAgentRuntime,
message: Memory,
state?: State,
options?: { [key: string]: unknown },
callback?: HandlerCallback,
responses?: Memory[]
) => Promise<unknown>;
Validator Function
The validator determines if an action should execute:
type Validator = (runtime: IAgentRuntime, message: Memory, state?: State) => Promise<boolean>;
HandlerCallback
The callback processes action results:
type HandlerCallback = (response: Content, files?: any) => Promise<Memory[]>;
ActionExample
Examples demonstrate action usage:
interface ActionExample {
/** User associated with the example */
name: string;
/** Content of the example */
content: Content;
}
Real-World Example: Reply Action
Let's examine the reply action from plugin-bootstrap:
export const replyAction = {
name: "REPLY",
similes: ["GREET", "REPLY_TO_MESSAGE", "SEND_REPLY", "RESPOND", "RESPONSE"],
description:
"Replies to the current conversation with the text from the generated message. Default if the agent is responding with a message and no other action. Use REPLY at the beginning of a chain of actions as an acknowledgement, and at the end of a chain of actions as a final response.",
validate: async (_runtime: IAgentRuntime) => {
return true; // Always valid
},
handler: async (
runtime: IAgentRuntime,
message: Memory,
state: State,
_options: any,
callback: HandlerCallback,
responses?: Memory[]
) => {
// Check if any responses had providers associated with them
const allProviders = responses?.flatMap((res) => res.content?.providers ?? []) ?? [];
// Compose state with providers from previous responses plus RECENT_MESSAGES
state = await runtime.composeState(message, [...(allProviders ?? []), "RECENT_MESSAGES"]);
const prompt = composePromptFromState({
state,
template: replyTemplate,
});
const response = await runtime.useModel(ModelType.OBJECT_LARGE, {
prompt,
});
const responseContent = {
thought: response.thought,
text: (response.message as string) || "",
actions: ["REPLY"],
};
await callback(responseContent);
return true;
},
examples: [
[
{
name: "{{name1}}",
content: {
text: "Hello there!",
},
},
{
name: "{{name2}}",
content: {
text: "Hi! How can I help you today?",
actions: ["REPLY"],
},
},
],
[
{
name: "{{name1}}",
content: {
text: "What's your favorite color?",
},
},
{
name: "{{name2}}",
content: {
text: "I really like deep shades of blue. They remind me of the ocean and the night sky.",
actions: ["REPLY"],
},
},
],
],
} as Action;
Reply Template
The reply action uses a template for generating responses:
const replyTemplate = `# Task: Generate dialog for the character {{agentName}}.
{{providers}}
# Instructions: Write the next message for {{agentName}}.
"thought" should be a short description of what the agent is thinking about and planning.
"message" should be the next message for {{agentName}} which they will send to the conversation.
Response format should be formatted in a valid JSON block like this:
\`\`\`json
{
"thought": "<string>",
"message": "<string>"
}
\`\`\`
Your response should include the valid JSON block and nothing else.`;
Understanding composeState in Actions
The composeState
method is crucial for providing context to actions. It accepts:
async composeState(
message: Memory,
includeList: string[] | null = null,
onlyInclude = false,
skipCache = false
): Promise<State>
Parameters:
- message: The current message being processed
- includeList: Array of provider names to include (e.g., ['RECENT_MESSAGES', 'CHARACTER'])
- onlyInclude: If true, ONLY includes providers in includeList; if false, includes all non-private providers plus those in includeList
- skipCache: If true, bypasses the state cache and fetches fresh data
Common Usage Patterns:
// Include specific providers only
state = await runtime.composeState(message, ['CHARACTER', 'RECENT_MESSAGES']);
// Include all default providers plus additional ones
state = await runtime.composeState(message, ['KNOWLEDGE', 'FACTS']);
// Dynamic provider inclusion based on previous responses
const allProviders = responses?.flatMap((res) => res.content?.providers ?? []) ?? [];
state = await runtime.composeState(message, [...allProviders, 'RECENT_MESSAGES']);
Action Implementation Patterns
Basic Action Structure
export const myAction: Action = {
name: "MY_ACTION",
description: "Description of what this action does",
validate: async (runtime: IAgentRuntime, message: Memory, state?: State) => {
// Validation logic
return true;
},
handler: async (
runtime: IAgentRuntime,
message: Memory,
state?: State,
options?: any,
callback?: HandlerCallback
) => {
// Implementation logic
const result = await performAction();
if (callback) {
await callback(result);
}
return result;
},
examples: [
// Example interactions
],
};
Validation Patterns
Simple Validation
validate: async (runtime: IAgentRuntime, message: Memory) => {
// Always execute
return true;
};
Conditional Validation
validate: async (runtime: IAgentRuntime, message: Memory) => {
// Only execute if user mentions specific keyword
return message.content.text?.includes("weather") || false;
};
State-Based Validation
validate: async (runtime: IAgentRuntime, message: Memory, state?: State) => {
// Execute based on current state
return state?.values?.userRole === "admin";
};
Complex Validation
validate: async (runtime: IAgentRuntime, message: Memory, state?: State) => {
// Multiple conditions
const hasPermission = await checkUserPermission(message.entityId);
const isInCorrectRoom = message.roomId === "support-room";
const isBusinessHours = new Date().getHours() >= 9 && new Date().getHours() <= 17;
return hasPermission && isInCorrectRoom && isBusinessHours;
};
Handler Patterns
Simple Response Handler
handler: async (runtime, message, state, options, callback) => {
const responseContent = {
text: "Action executed successfully",
actions: ["MY_ACTION"],
};
await callback?.(responseContent);
return true;
};
LLM-Based Handler
handler: async (runtime, message, state, options, callback) => {
const prompt = composePromptFromState({
state,
template: myTemplate,
});
const response = await runtime.useModel(ModelType.OBJECT_LARGE, {
prompt,
});
const responseContent = {
thought: response.thought,
text: response.message,
actions: ["MY_ACTION"],
};
await callback?.(responseContent);
return response;
};
Data Processing Handler
handler: async (runtime, message, state, options, callback) => {
// Process data
const data = await fetchExternalData();
const processedData = processData(data);
// Store processed data in memory for future use
await runtime.createMemory({
id: crypto.randomUUID() as UUID,
content: {
text: `Processed ${processedData.length} items`,
data: processedData,
},
entityId: message.entityId,
roomId: message.roomId,
type: "processing_result",
});
const responseContent = {
text: `Processed ${processedData.length} items`,
data: processedData,
actions: ["MY_ACTION"],
};
await callback?.(responseContent);
return processedData;
};
More Action Examples from Plugin-Bootstrap
Send Message Action (from plugin-bootstrap)
export const sendMessageAction: Action = {
name: "SEND_MESSAGE",
similes: ["MESSAGE", "SEND", "POST", "PUBLISH"],
description: "Send a message to a specific channel",
validate: async (runtime: IAgentRuntime, message: Memory, state?: State) => {
// Only validate if there's a target room or channel specified
return !!state?.values?.targetRoomId || !!options?.targetRoomId;
},
handler: async (runtime, message, state, options, callback) => {
const targetRoomId = options?.targetRoomId || state?.values?.targetRoomId;
const messageText = options?.text || state?.values?.messageText;
if (!messageText) {
throw new Error("No message text provided");
}
if (!targetRoomId) {
throw new Error("No target room specified");
}
// Create a message memory in the target room
const messageMemory = {
id: crypto.randomUUID() as UUID,
content: {
text: messageText,
actions: ["SEND_MESSAGE"],
},
entityId: runtime.agentId,
roomId: targetRoomId,
createdAt: Date.now(),
};
await runtime.createMemory(messageMemory);
// If there's a send handler registered for the platform, use it
const room = await runtime.getRoom(targetRoomId);
if (room?.source) {
await runtime.sendMessageToTarget(
{
roomId: targetRoomId,
channelId: room.channelId,
serverId: room.serverId,
},
messageMemory.content
);
}
const responseContent = {
text: `Message sent to room ${targetRoomId}`,
actions: ["SEND_MESSAGE"],
};
await callback?.(responseContent);
return true;
},
examples: [
[
{
name: "User",
content: { text: "Send a message to the general channel" },
},
{
name: "Agent",
content: {
text: "I'll send that message to the general channel",
actions: ["SEND_MESSAGE"],
},
},
],
],
};
Update Entity Action (from plugin-bootstrap)
The actual updateEntityAction
in plugin-bootstrap updates entity components (contact information across different platforms), not the entity itself:
export const updateEntityAction: Action = {
name: "UPDATE_CONTACT",
similes: ["UPDATE_ENTITY"],
description:
"Add or edit contact details for a person you are talking to or observing in the conversation. Use this when you learn this information from the conversation about a contact. This is for the agent to relate entities across platforms, not for world settings or configuration.",
validate: async (_runtime: IAgentRuntime, _message: Memory, _state?: State) => {
return true; // Always valid
},
handler: async (runtime, message, state, options, callback, responses) => {
// Handle initial responses
for (const response of responses) {
await callback(response.content);
}
const sourceEntityId = message.entityId;
const agentId = runtime.agentId;
const room = state.data.room ?? (await runtime.getRoom(message.roomId));
const worldId = room.worldId;
// First, find the entity being referenced
const entity = await findEntityByName(runtime, message, state);
if (!entity) {
await callback({
text: "I'm not sure which entity you're trying to update. Could you please specify who you're talking about?",
actions: ["UPDATE_ENTITY_ERROR"],
source: message.content.source,
});
return;
}
// Use LLM to extract platform and component data from conversation
const prompt = composePromptFromState({
state,
template: componentTemplate, // Template to extract source and data
});
const result = await runtime.useModel(ModelType.TEXT_LARGE, {
prompt,
stopSequences: [],
});
// Parse the generated data
const parsedResult = JSON.parse(result.match(/\{[\s\S]*\}/)[0]);
const componentType = parsedResult.source.toLowerCase();
const componentData = parsedResult.data;
// Check if component exists
const existingComponent = await runtime.getComponent(
entity.id!,
componentType,
worldId,
sourceEntityId
);
if (existingComponent) {
// Update existing component
await runtime.updateComponent({
id: existingComponent.id,
entityId: entity.id!,
worldId,
type: componentType,
data: componentData,
agentId,
roomId: message.roomId,
sourceEntityId,
createdAt: existingComponent.createdAt,
});
await callback({
text: `I've updated the ${componentType} information for ${entity.names[0]}.`,
actions: ["UPDATE_ENTITY"],
source: message.content.source,
});
} else {
// Create new component
await runtime.createComponent({
id: uuidv4() as UUID,
entityId: entity.id!,
worldId,
type: componentType,
data: componentData,
agentId,
roomId: message.roomId,
sourceEntityId,
createdAt: Date.now(),
});
await callback({
text: `I've added new ${componentType} information for ${entity.names[0]}.`,
actions: ["UPDATE_ENTITY"],
source: message.content.source,
});
}
},
examples: [
[
{
name: "{{name1}}",
content: {
text: "Please update my telegram username to @dev_guru",
},
},
{
name: "{{name2}}",
content: {
text: "I've updated your telegram information.",
actions: ["UPDATE_ENTITY"],
},
},
],
[
{
name: "{{name1}}",
content: {
text: "Set Jimmy's twitter username to @jimmy_codes",
},
},
{
name: "{{name2}}",
content: {
text: "I've updated Jimmy's twitter information.",
actions: ["UPDATE_ENTITY"],
},
},
],
],
};
Settings Action
export const updateSettingsAction = {
name: "UPDATE_SETTINGS",
similes: ["CONFIGURE", "SETTINGS", "PREFERENCES", "OPTIONS"],
description: "Update agent or system settings",
validate: async (runtime: IAgentRuntime, message: Memory) => {
// Check if user has admin privileges
return message.content.text?.includes("settings") || false;
},
handler: async (runtime, message, state, options, callback) => {
const settings = options?.settings || {};
// Update settings
await runtime.updateSettings(settings);
const responseContent = {
text: "Settings updated successfully",
actions: ["UPDATE_SETTINGS"],
};
await callback?.(responseContent);
return true;
},
examples: [
[
{
name: "User",
content: { text: "Update my notification settings" },
},
{
name: "Agent",
content: {
text: "Your notification settings have been updated",
actions: ["UPDATE_SETTINGS"],
},
},
],
],
};
Advanced Action Patterns
Chained Actions
Actions can be chained together for complex workflows:
export const complexWorkflowAction = {
name: "COMPLEX_WORKFLOW",
description: "Execute a complex multi-step workflow",
validate: async (runtime, message) => {
return message.content.text?.includes("start workflow") || false;
},
handler: async (runtime, message, state, options, callback) => {
const steps = ["VALIDATE_INPUT", "PROCESS_DATA", "SEND_NOTIFICATION", "UPDATE_RECORDS"];
let results = [];
for (const step of steps) {
const action = runtime.getAction(step);
if (action) {
const result = await action.handler(runtime, message, state, options, callback);
results.push(result);
}
}
const responseContent = {
text: `Workflow completed with ${results.length} steps`,
actions: ["COMPLEX_WORKFLOW"],
};
await callback?.(responseContent);
return results;
},
examples: [
[
{
name: "User",
content: { text: "Start workflow for new customer" },
},
{
name: "Agent",
content: {
text: "Workflow initiated for new customer processing",
actions: ["COMPLEX_WORKFLOW"],
},
},
],
],
};
Conditional Actions
Actions that execute different logic based on conditions:
export const conditionalAction = {
name: "CONDITIONAL_ACTION",
description: "Execute different logic based on user type",
validate: async (runtime, message) => {
return true;
},
handler: async (runtime, message, state, options, callback) => {
const userType = state?.values?.userType || "guest";
let responseContent;
switch (userType) {
case "admin":
responseContent = {
text: "Admin features enabled",
actions: ["CONDITIONAL_ACTION", "ADMIN_FEATURES"],
};
break;
case "premium":
responseContent = {
text: "Premium features available",
actions: ["CONDITIONAL_ACTION", "PREMIUM_FEATURES"],
};
break;
default:
responseContent = {
text: "Basic features available",
actions: ["CONDITIONAL_ACTION", "BASIC_FEATURES"],
};
}
await callback?.(responseContent);
return responseContent;
},
examples: [
[
{
name: "Admin",
content: { text: "What can I do?" },
},
{
name: "Agent",
content: {
text: "Admin features enabled - you have full access",
actions: ["CONDITIONAL_ACTION"],
},
},
],
],
};
Best Practices
Action Design
- Single Responsibility: Each action should have a clear, single purpose
- Clear Naming: Use descriptive names that indicate the action's function
- Good Validation: Implement proper validation to prevent unnecessary execution
- Error Handling: Handle errors gracefully and provide meaningful feedback
Validation Guidelines
// Good: Specific validation
validate: async (runtime, message) => {
return message.content.text?.toLowerCase().includes("weather") || false;
};
// Better: Comprehensive validation
validate: async (runtime, message, state) => {
const hasWeatherKeyword = message.content.text?.toLowerCase().includes("weather");
const hasLocation = state?.values?.location;
const hasApiKey = runtime.getSetting("weather_api_key");
return hasWeatherKeyword && hasLocation && hasApiKey;
};
Handler Implementation
// Good: Error handling
handler: async (runtime, message, state, options, callback) => {
try {
const result = await performAction();
const responseContent = {
text: "Action completed successfully",
data: result,
actions: ["MY_ACTION"],
};
await callback?.(responseContent);
return result;
} catch (error) {
const errorContent = {
text: `Action failed: ${error.message}`,
actions: ["ERROR"],
};
await callback?.(errorContent);
throw error;
}
};
Example Quality
// Good: Comprehensive examples
examples: [
// Simple case
[
{
name: "User",
content: { text: "What is the weather?" },
},
{
name: "Agent",
content: {
text: "The weather is sunny and 72°F",
actions: ["WEATHER"],
},
},
],
// Complex case
[
{
name: "User",
content: { text: "What is the weather forecast for tomorrow in New York?" },
},
{
name: "Agent",
content: {
text: "Tomorrow in New York: Partly cloudy with a high of 68°F and low of 55°F",
actions: ["WEATHER"],
},
},
],
];
Testing Actions
Unit Testing
describe("replyAction", () => {
it("should validate correctly", async () => {
const runtime = mockRuntime();
const message = mockMessage();
const isValid = await replyAction.validate(runtime, message);
expect(isValid).toBe(true);
});
it("should handle message properly", async () => {
const runtime = mockRuntime();
const message = mockMessage();
const state = mockState();
const callback = jest.fn();
await replyAction.handler(runtime, message, state, {}, callback);
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
text: expect.any(String),
actions: ["REPLY"],
})
);
});
});
Integration Testing
describe("Action Integration", () => {
it("should work with runtime", async () => {
const runtime = new AgentRuntime({
character: mockCharacter(),
// ... other config
});
runtime.registerAction(myAction);
const message = await runtime.processMessage(mockMessage());
expect(message.content.actions).toContain("MY_ACTION");
});
});
Common Pitfalls
Validation Issues
// Bad: Overly broad validation
validate: async () => true;
// Bad: No error handling
validate: async (runtime, message) => {
return message.content.text.includes("keyword"); // Can throw if text is null
};
// Good: Proper validation
validate: async (runtime, message) => {
return message.content.text?.includes("keyword") || false;
};
Handler Problems
// Bad: No error handling
handler: async (runtime, message, state, options, callback) => {
const data = await fetchData(); // Can throw
await callback({ text: data.result });
};
// Good: Proper error handling
handler: async (runtime, message, state, options, callback) => {
try {
const data = await fetchData();
await callback({ text: data.result, actions: ["MY_ACTION"] });
} catch (error) {
await callback({ text: "Failed to fetch data", actions: ["ERROR"] });
throw error;
}
};
Related Components
- Agents: How actions are executed within agent runtime
- Evaluators: Assessment systems that complement actions
- Providers: Data sources for action context
- Character Definition: How character traits influence actions
Summary
Actions are the executable behaviors that give agents their interactive capabilities. They combine validation logic, handler implementation, and examples to create robust, reusable components. The plugin-bootstrap package provides excellent examples of real-world action implementations that demonstrate best practices for validation, error handling, and user interaction patterns. Well-designed actions form the foundation of engaging agent behaviors in ElizaOS.
Character Definition
Complete guide to creating and validating character definitions with schema validation, bio management, and configuration patterns in ElizaOS
Evaluators
Understanding evaluators in ElizaOS, including the Evaluator interface, reflection patterns, and behavior assessment systems with real examples from plugin-bootstrap