--- 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. Multi-step workflows where actions build on each other Send progress updates before actions complete Combine multiple providers elegantly Graceful degradation and retry strategies ## 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/patterns#the-actionresult-interface). For component basics, see [Plugin Components](/plugins/architecture). ### 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; ``` Example usage: ```typescript async handler( runtime: IAgentRuntime, message: Memory, _state?: State, _options?: Record, callback?: HandlerCallback ): Promise { 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, callback?: HandlerCallback ): Promise { // 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 }, }; ``` You don't need to import or configure the basic-capabilities plugin - it's automatically included when you create an AgentRuntime. ### 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) { // 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; } }, }; ``` **Guides**: [Create a Plugin](/plugins/create-a-plugin) | [Publish a Plugin](/plugins/publish) ## See Also Understand the overall plugin system design Build your first plugin with step-by-step guidance Learn about configuration and validation schemas Migrate existing plugins to new patterns