Back to Eliza

Patterns

packages/docs/plugins/patterns.mdx

1.7.225.1 KB
Original Source

Real Patterns from Real Plugins

These aren't theoretical - they're extracted from production plugins. Each pattern solves a specific problem you'll encounter when building agents that actually work.

<CardGroup cols={2}> <Card title="Action Chaining" icon="link"> Multi-step workflows where actions build on each other </Card> <Card title="Callbacks" icon="reply"> Send progress updates before actions complete </Card> <Card title="State Composition" icon="layer-group"> Combine multiple providers elegantly </Card> <Card title="Error Recovery" icon="rotate-right"> Graceful degradation and retry strategies </Card> </CardGroup>

Action Chaining and Callbacks

Action chaining allows multiple actions to execute sequentially, with each action accessing previous results. This enables complex workflows where actions build upon each other's outputs.

The ActionResult Interface

Actions return an ActionResult object that standardizes how actions communicate their outcomes. This interface includes:

  • success (required): Boolean indicating whether the action completed successfully
  • text: Optional human-readable description of the result
  • values: Key-value pairs to merge into the state for subsequent actions
  • data: Raw data payload with action-specific results
  • error: Error information if the action failed

The success field is the only required field, making it easy to create simple results while supporting complex data passing for action chaining.

For interface definitions, see Plugin Reference. For component basics, see Plugin Components.

Handler Callbacks

The HandlerCallback provides a mechanism for actions to send immediate feedback to users before the action completes:

typescript
export type HandlerCallback = (
  response: Content,
  files?: Attachment[],
) => Promise<Memory[]>;

Example usage:

typescript
async handler(
  runtime: IAgentRuntime,
  message: Memory,
  _state?: State,
  _options?: Record<string, unknown>,
  callback?: HandlerCallback
): Promise<ActionResult> {
  try {
    // Send immediate feedback
    await callback?.({
      text: `Starting to process your request...`,
      source: message.content.source
    });

    // Perform action logic
    const result = await performComplexOperation();

    // Send success message to user via callback
    await callback?.({
      text: `Created issue: ${result.title} (${result.identifier})\n\nView it at: ${result.url}`,
      source: message.content.source
    });

    // Return structured result for potential chaining
    return {
      success: true,
      text: `Created issue: ${result.title}`,
      data: {
        issueId: result.id,
        identifier: result.identifier,
        url: result.url
      }
    };
  } catch (error) {
    // Send error message to user
    await callback?.({
      text: `Failed to create issue: ${error.message}`,
      source: message.content.source
    });

    return {
      success: false,
      text: `Failed to create issue: ${error.message}`,
      error: error instanceof Error ? error : new Error(String(error))
    };
  }
}

Action Context and Previous Results

When multiple actions are executed in sequence, each action receives an ActionContext that provides access to previous action results:

typescript
export interface ActionContext {
  /** Results from previously executed actions in this run */
  previousResults: ActionResult[];

  /** Get a specific previous result by action name */
  getPreviousResult?: (actionName: string) => ActionResult | undefined;
}

The runtime automatically provides this context in the options parameter:

typescript
async handler(
  runtime: IAgentRuntime,
  message: Memory,
  state?: State,
  options?: Record<string, unknown>,
  callback?: HandlerCallback
): Promise<ActionResult> {
  // Access the action context
  const context = options?.context as ActionContext;

  // Get results from a specific previous action
  const previousResult = context?.getPreviousResult?.('CREATE_LINEAR_ISSUE');

  if (previousResult?.data?.issueId) {
    // Use data from previous action
    const issueId = previousResult.data.issueId;
    // ... continue with logic using previous result ...
  }
}

Action Execution Flow

The runtime's processActions method manages the execution flow:

  1. Action Planning: When multiple actions are detected, the runtime creates an execution plan
  2. Sequential Execution: Actions execute in the order specified by the agent
  3. State Accumulation: Each action's results are merged into the accumulated state
  4. Working Memory: Results are stored in working memory for access during execution
  5. Error Handling: Failed actions don't stop the chain unless marked as critical

Working Memory Management

The runtime maintains a working memory that stores recent action results:

typescript
// Results are automatically stored in state.data.workingMemory
const memoryEntry: WorkingMemoryEntry = {
  actionName: action.name,
  result: actionResult,
  timestamp: Date.now(),
};

The system keeps the most recent 50 entries (configurable) to prevent memory bloat.

Action Patterns

Decision-Making Actions

Actions can use the LLM to make intelligent decisions based on context:

typescript
export const muteRoomAction: Action = {
  name: "MUTE_ROOM",
  similes: ["SHUT_UP", "BE_QUIET", "STOP_TALKING", "SILENCE"],
  description: "Mutes a room if asked to or if the agent is being annoying",

  validate: async (runtime, message) => {
    // Check if already muted
    const roomState = await runtime.getParticipantUserState(
      message.roomId,
      runtime.agentId,
    );
    return roomState !== "MUTED";
  },

  handler: async (runtime, message, state) => {
    // Create a decision prompt
    const shouldMuteTemplate = `# Task: Should {{agentName}} mute this room?

{{recentMessages}}

Should {{agentName}} mute and stop responding unless mentioned?

Respond YES if:
- User asked to stop/be quiet
- Agent responses are annoying users
- Conversation is hostile

Otherwise NO.`;

    const prompt = composePromptFromState({
      state,
      template: shouldMuteTemplate,
    });
    const decision = await runtime.useModel(ModelType.TEXT_SMALL, {
      prompt,
      runtime,
    });

    if (decision.toLowerCase().includes("yes")) {
      await runtime.setParticipantUserState(
        message.roomId,
        runtime.agentId,
        "MUTED",
      );

      return {
        success: true,
        text: "Going silent in this room",
        values: { roomMuted: true },
      };
    }

    return {
      success: false,
      text: "Continuing to participate",
    };
  },
};

Multi-Step Actions

Actions that need to perform multiple steps with intermediate feedback:

typescript
export const deployContractAction: Action = {
  name: "DEPLOY_CONTRACT",
  description: "Deploy a smart contract with multiple steps",

  handler: async (runtime, message, state, options, callback) => {
    try {
      // Step 1: Compile
      await callback?.({
        text: "📝 Compiling contract...",
        actions: ["DEPLOY_CONTRACT"],
      });
      const compiled = await compileContract(state.contractCode);

      // Step 2: Estimate gas
      await callback?.({
        text: "⛽ Estimating gas costs...",
        actions: ["DEPLOY_CONTRACT"],
      });
      const gasEstimate = await estimateGas(compiled);

      // Step 3: Deploy
      await callback?.({
        text: `🚀 Deploying with gas: ${gasEstimate}...`,
        actions: ["DEPLOY_CONTRACT"],
      });
      const deployed = await deploy(compiled, gasEstimate);

      // Step 4: Verify
      await callback?.({
        text: "✅ Verifying deployment...",
        actions: ["DEPLOY_CONTRACT"],
      });
      await verifyContract(deployed.address);

      return {
        success: true,
        text: `Contract deployed at ${deployed.address}`,
        values: {
          contractAddress: deployed.address,
          transactionHash: deployed.txHash,
          gasUsed: deployed.gasUsed,
        },
        data: {
          deployment: deployed,
          verification: true,
        },
      };
    } catch (error) {
      return {
        success: false,
        text: `Deployment failed: ${error.message}`,
        error,
      };
    }
  },
};

API Integration Actions

Pattern for external API calls with retries and error handling:

typescript
export const apiAction: Action = {
  name: "API_CALL",

  handler: async (runtime, message, state, options, callback) => {
    const maxRetries = 3;
    let lastError: Error | null = null;

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        await callback?.({
          text: `Attempt ${attempt}/${maxRetries}...`,
        });

        const result = await callExternalAPI({
          endpoint: state.endpoint,
          data: state.data,
          timeout: 5000,
        });

        return {
          success: true,
          text: "API call successful",
          data: result,
        };
      } catch (error) {
        lastError = error as Error;

        if (attempt < maxRetries) {
          await callback?.({
            text: `Attempt ${attempt} failed, retrying...`,
          });
          await new Promise((r) => setTimeout(r, 1000 * attempt)); // Exponential backoff
        }
      }
    }

    return {
      success: false,
      text: `API call failed after ${maxRetries} attempts`,
      error: lastError,
    };
  },
};

Context-Aware Actions

Actions that adapt based on conversation context:

typescript
export const contextAwareAction: Action = {
  name: "CONTEXT_RESPONSE",

  handler: async (runtime, message, state, options, callback) => {
    // Analyze conversation sentiment
    const sentiment = await analyzeSentiment(state.recentMessages);

    // Adjust response based on context
    let responseStrategy: string;
    if (sentiment.score < -0.5) {
      responseStrategy = "empathetic";
    } else if (sentiment.score > 0.5) {
      responseStrategy = "enthusiastic";
    } else {
      responseStrategy = "neutral";
    }

    // Generate context-appropriate response
    const response = await generateResponse(state, responseStrategy, runtime);

    return {
      success: true,
      text: response.text,
      values: {
        sentiment: sentiment.score,
        strategy: responseStrategy,
      },
    };
  },
};

Action Composition

Compose Multiple Actions

typescript
// Compose multiple actions into higher-level operations
export const compositeAction: Action = {
  name: "SEND_AND_TRACK",
  description: "Send a message and track its delivery",

  handler: async (runtime, message, state, options, callback) => {
    // Execute sub-actions
    const sendResult = await sendMessageAction.handler(
      runtime,
      message,
      state,
      options,
      callback,
    );

    if (!sendResult.success) {
      return sendResult; // Propagate failure
    }

    // Track the sent message
    const trackingId = generateTrackingId();
    await runtime.createMemory(
      {
        id: trackingId,
        entityId: message.entityId,
        roomId: message.roomId,
        content: {
          type: "message_tracking",
          sentTo: sendResult.data.target,
          sentAt: Date.now(),
          messageContent: sendResult.data.messageContent,
        },
      },
      "tracking",
    );

    return {
      success: true,
      text: `Message sent and tracked (${trackingId})`,
      values: {
        ...sendResult.values,
        trackingId,
        tracked: true,
      },
      data: {
        sendResult,
        trackingId,
      },
    };
  },
};

Workflow Orchestration

typescript
export const workflowAction: Action = {
  name: "COMPLEX_WORKFLOW",

  handler: async (runtime, message, state, options, callback) => {
    const workflow = [
      { action: "VALIDATE_INPUT", required: true },
      { action: "FETCH_DATA", required: true },
      { action: "PROCESS_DATA", required: false },
      { action: "STORE_RESULTS", required: true },
      { action: "NOTIFY_USER", required: false },
    ];

    const results: ActionResult[] = [];

    for (const step of workflow) {
      const action = runtime.getAction(step.action);
      if (!action) {
        if (step.required) {
          return {
            success: false,
            text: `Required action ${step.action} not found`,
          };
        }
        continue;
      }

      const result = await action.handler(
        runtime,
        message,
        state,
        { context: { previousResults: results } },
        callback,
      );

      results.push(result);

      if (!result.success && step.required) {
        return {
          success: false,
          text: `Workflow failed at ${step.action}`,
          data: { failedStep: step.action, results },
        };
      }

      // Merge values into state for next action
      state = {
        ...state,
        values: {
          ...state.values,
          ...result.values,
        },
      };
    }

    return {
      success: true,
      text: "Workflow completed successfully",
      data: { workflowResults: results },
    };
  },
};

Self-Modifying Actions

Actions that learn and adapt their behavior:

typescript
export const learningAction: Action = {
  name: "ADAPTIVE_RESPONSE",

  handler: async (runtime, message, state) => {
    // Retrieve past performance
    const history = await runtime.getMemories({
      tableName: "action_feedback",
      roomId: message.roomId,
      count: 100,
    });

    // Analyze what worked well
    const analysis = await runtime.useModel(ModelType.TEXT_LARGE, {
      prompt: `Analyze these past interactions and identify patterns:
${JSON.stringify(history)}
What response strategies were most effective?`,
    });

    // Adapt behavior based on learning
    const strategy = determineStrategy(analysis);
    const response = await generateResponse(state, strategy);

    // Store for future learning
    await runtime.createMemory(
      {
        id: generateId(),
        content: {
          type: "action_feedback",
          strategy: strategy.name,
          context: state.text,
          response: response.text,
        },
      },
      "action_feedback",
    );

    return {
      success: true,
      text: response.text,
      values: {
        strategyUsed: strategy.name,
        confidence: strategy.confidence,
      },
    };
  },
};

Provider Patterns

Conditional Providers

Providers that only provide data under certain conditions:

typescript
export const conditionalProvider: Provider = {
  name: "PREMIUM_DATA",
  private: true,

  get: async (runtime, message, state) => {
    // Check if user has premium access
    const user = await runtime.getUser(message.entityId);

    if (!user.isPremium) {
      return {
        text: "",
        values: {},
        data: { available: false },
      };
    }

    // Provide premium data
    const premiumData = await fetchPremiumData(user);

    return {
      text: formatPremiumData(premiumData),
      values: premiumData,
      data: { available: true, ...premiumData },
    };
  },
};

Aggregating Providers

Providers that combine data from multiple sources:

typescript
export const aggregateProvider: Provider = {
  name: "MARKET_OVERVIEW",
  position: 50, // Run after individual data providers

  get: async (runtime, message, state) => {
    // Aggregate from multiple sources
    const [stocks, crypto, forex] = await Promise.all([
      fetchStockData(),
      fetchCryptoData(),
      fetchForexData(),
    ]);

    const overview = {
      stocksUp: stocks.filter((s) => s.change > 0).length,
      stocksDown: stocks.filter((s) => s.change < 0).length,
      cryptoMarketCap: crypto.reduce((sum, c) => sum + c.marketCap, 0),
      forexVolatility: calculateVolatility(forex),
    };

    return {
      text: `Market Overview:
- Stocks: ${overview.stocksUp} up, ${overview.stocksDown} down
- Crypto Market Cap: $${overview.cryptoMarketCap.toLocaleString()}
- Forex Volatility: ${overview.forexVolatility}`,
      values: overview,
      data: { stocks, crypto, forex },
    };
  },
};

Best Practices for Action Chaining

  1. Always Return ActionResult: Even for simple actions, return a proper ActionResult object:

    typescript
    return {
      success: true,
      text: "Action completed",
      data: {
        /* any data for next actions */
      },
    };
    
  2. Use Callbacks for User Feedback: Send immediate feedback via callbacks rather than waiting for the action to complete:

    typescript
    await callback?.({
      text: "Processing your request...",
      source: message.content.source,
    });
    
  3. Store Identifiers in Data: When creating resources, store identifiers that subsequent actions might need:

    typescript
    return {
      success: true,
      data: {
        resourceId: created.id,
        resourceUrl: created.url,
      },
    };
    
  4. Handle Missing Dependencies: Check if required previous results exist:

    typescript
    const previousResult = context?.getPreviousResult?.("REQUIRED_ACTION");
    if (!previousResult?.success) {
      return {
        success: false,
        text: "Required previous action did not complete successfully",
      };
    }
    
  5. Use ActionResult: All actions must return ActionResult with a success field.

Example: Multi-Step Workflow

Here's an example of a multi-step workflow using action chaining:

typescript
// User: "Create a bug report for the login issue and assign it to John"
// Agent executes: REPLY, CREATE_LINEAR_ISSUE, UPDATE_LINEAR_ISSUE

// Action 1: CREATE_LINEAR_ISSUE
{
  success: true,
  data: {
    issueId: "abc-123",
    identifier: "BUG-456"
  }
}

// Action 2: UPDATE_LINEAR_ISSUE (can access previous result)
async handler(runtime, message, state, options, callback) {
  const context = options?.context as ActionContext;
  const createResult = context?.getPreviousResult?.('CREATE_LINEAR_ISSUE');

  if (createResult?.data?.issueId) {
    // Use the issue ID from previous action
    await updateIssue(createResult.data.issueId, { assignee: "John" });

    return {
      success: true,
      text: "Issue assigned to John"
    };
  }
}

Common Patterns

  1. Create and Configure: Create a resource, then configure it
  2. Search and Update: Find resources, then modify them
  3. Validate and Execute: Check conditions, then perform actions
  4. Aggregate and Report: Collect data from multiple sources, then summarize

The action chaining system provides a powerful way to build complex, multi-step workflows while maintaining clean separation between individual actions.

Real-World Implementation Patterns

This section documents actual patterns and structures used in production elizaOS plugins based on examination of real implementations.

Basic Plugin Structure Pattern

Every plugin follows this core structure pattern (from plugin-starter):

typescript
import type { Plugin } from "@elizaos/core";

export const myPlugin: Plugin = {
  name: "plugin-name",
  description: "Plugin description",

  // Core components
  actions: [], // Actions the plugin provides
  providers: [], // Data providers
  services: [], // Background services
  evaluators: [], // Response evaluators

  // Optional components
  init: async (config) => {}, // Initialization logic
  models: {}, // Custom model implementations
  routes: [], // HTTP routes
  events: {}, // Event handlers
  tests: [], // Test suites
  dependencies: [], // Other required plugins
};

BasicCapabilities Plugin Pattern

The basic-capabilities plugin is built into @elizaos/core and automatically registered during runtime initialization. It provides core functionality through:

typescript
// The basic-capabilities plugin is created internally using createBasicCapabilitiesPlugin()
// It provides:
const basicCapabilities = {
  actions: [
    actions.replyAction,
    actions.followRoomAction,
    actions.ignoreAction,
    actions.sendMessageAction,
    actions.generateImageAction,
    // ... more actions
  ],
  providers: [
    providers.timeProvider,
    providers.entitiesProvider,
    providers.characterProvider,
    providers.recentMessagesProvider,
    // ... more providers
  ],
  services: [TaskService],
  evaluators: [evaluators.reflectionEvaluator],
  events: {
    [EventType.MESSAGE_RECEIVED]: [messageReceivedHandler],
    [EventType.POST_GENERATED]: [postGeneratedHandler],
    // ... more event handlers
  },
};
<Note> You don't need to import or configure the basic-capabilities plugin - it's automatically included when you create an AgentRuntime. </Note>

Service Plugin Pattern (Discord, Telegram)

Platform integration plugins focus on service implementation:

typescript
// Discord Plugin
const discordPlugin: Plugin = {
  name: "discord",
  description: "Discord service plugin for integration with Discord servers",
  services: [DiscordService],
  actions: [
    chatWithAttachments,
    downloadMedia,
    joinVoice,
    leaveVoice,
    summarize,
    transcribeMedia,
  ],
  providers: [channelStateProvider, voiceStateProvider],
  tests: [new DiscordTestSuite()],
  init: async (config, runtime) => {
    // Check for required API tokens
    const token = runtime.getSetting("DISCORD_API_TOKEN");
    if (!token) {
      logger.warn("Discord API Token not provided");
    }
  },
};

// Telegram Plugin (minimal)
const telegramPlugin: Plugin = {
  name: TELEGRAM_SERVICE_NAME,
  description: "Telegram client plugin",
  services: [TelegramService],
  tests: [new TelegramTestSuite()],
};

Action Implementation Pattern

Actions follow a consistent structure with validation and execution:

typescript
const helloWorldAction: Action = {
  name: "HELLO_WORLD",
  similes: ["GREET", "SAY_HELLO"], // Alternative names
  description: "Responds with a simple hello world message",

  validate: async (runtime, message, state) => {
    // Return true if action can be executed
    return true;
  },

  handler: async (runtime, message, state, options, callback, responses) => {
    try {
      const responseContent: Content = {
        text: "hello world!",
        actions: ["HELLO_WORLD"],
        source: message.content.source,
      };

      if (callback) {
        await callback(responseContent);
      }

      return responseContent;
    } catch (error) {
      logger.error("Error in HELLO_WORLD action:", error);
      throw error;
    }
  },

  examples: [
    [
      {
        name: "{{name1}}",
        content: { text: "Can you say hello?" },
      },
      {
        name: "{{name2}}",
        content: {
          text: "hello world!",
          actions: ["HELLO_WORLD"],
        },
      },
    ],
  ],
};

Complex Action Pattern (Reply Action)

typescript
export const replyAction = {
  name: "REPLY",
  similes: ["GREET", "REPLY_TO_MESSAGE", "SEND_REPLY", "RESPOND"],
  description: "Replies to the current conversation",

  validate: async (runtime) => true,

  handler: async (runtime, message, state, options, callback, responses) => {
    // Compose state with providers
    state = await runtime.composeState(message, ["RECENT_MESSAGES"]);

    // Generate response using LLM
    const prompt = composePromptFromState({ state, template: replyTemplate });
    const response = await runtime.useModel(ModelType.OBJECT_LARGE, { prompt });

    const responseContent = {
      thought: response.thought,
      text: response.message || "",
      actions: ["REPLY"],
    };

    await callback(responseContent);
    return true;
  },
};

Provider Implementation Pattern

Providers supply contextual data to the agent:

typescript
export const timeProvider: Provider = {
  name: "TIME",
  get: async (runtime, message) => {
    const currentDate = new Date();
    const options = {
      timeZone: "UTC",
      dateStyle: "full" as const,
      timeStyle: "long" as const,
    };
    const humanReadable = new Intl.DateTimeFormat("en-US", options).format(
      currentDate,
    );

    return {
      data: { time: currentDate },
      values: { time: humanReadable },
      text: `The current date and time is ${humanReadable}.`,
    };
  },
};

Plugin Initialization Pattern

Plugins can have initialization logic:

typescript
const myPlugin: Plugin = {
  name: "my-plugin",
  config: {
    EXAMPLE_VARIABLE: process.env.EXAMPLE_VARIABLE,
  },
  async init(config: Record<string, string>) {
    // Validate configuration
    const validatedConfig = await configSchema.parseAsync(config);

    // Set environment variables
    for (const [key, value] of Object.entries(validatedConfig)) {
      if (value) process.env[key] = value;
    }
  },
};
<Tip> **Guides**: [Create a Plugin](/guides/create-a-plugin) | [Publish a Plugin](/guides/publish-a-plugin) </Tip>

See Also

<CardGroup cols={2}> <Card title="Plugin Architecture" icon="sitemap" href="/plugins/architecture"> Understand the overall plugin system design </Card> <Card title="Development Guide" icon="code" href="/plugins/development"> Build your first plugin with step-by-step guidance </Card> <Card title="Plugin Schemas" icon="list" href="/plugins/schemas"> Learn about configuration and validation schemas </Card> <Card title="Plugin Migration" icon="arrow-right" href="/plugins/migration"> Migrate existing plugins to new patterns </Card> </CardGroup>