---
title: "Patterns"
description: "Battle-tested patterns for building robust plugins"
---

## 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](/plugins/reference#action-interface). For component basics, see [Plugin Components](/plugins/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 planned-tool executor 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.factMemoryEvaluator,
    evaluators.relationshipEvaluator,
    evaluators.identityEvaluator,
    evaluators.successEvaluator,
  ],
  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>
