elizaOS

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

  1. Single Responsibility: Each action should have a clear, single purpose
  2. Clear Naming: Use descriptive names that indicate the action's function
  3. Good Validation: Implement proper validation to prevent unnecessary execution
  4. 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;
  }
};

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.