robotadocs
← robota.io

Building Plugins

Robota's plugin system lets you hook into the agent execution lifecycle without modifying core packages. Plugins are the right tool for cross-cutting concerns: logging, metrics, cost tracking, notifications, and audit trails.


Plugin Types

There are two ways to extend Robota:

ApproachWhen to use
AbstractPluginFull lifecycle access — before/after each run, tool calls, errors
EventEmitterPlugin listenersSubscribe to named events without subclassing

Quick Start — AbstractPlugin

import { AbstractPlugin, PluginCategory, PluginPriority } from '@robota-sdk/agent-core';
import type {
  IPluginExecutionContext,
  IPluginExecutionResult,
  IPluginErrorContext,
  IPluginOptions,
  IPluginStats,
} from '@robota-sdk/agent-core';
 
interface IMyPluginOptions extends IPluginOptions {
  logLevel?: 'info' | 'debug';
}
 
interface IMyPluginStats extends IPluginStats {
  requestCount: number;
}
 
export class MyPlugin extends AbstractPlugin<IMyPluginOptions, IMyPluginStats> {
  name = 'MyPlugin';
  version = '1.0.0';
  category = PluginCategory.MONITORING;
  priority = PluginPriority.NORMAL;
 
  // Called before each agent run
  async beforeExecution(context: IPluginExecutionContext): Promise<void> {
    this.updateCallStats();
    console.log(`[MyPlugin] Starting run: ${context.executionId}`);
  }
 
  // Called after each successful run
  async afterExecution(
    context: IPluginExecutionContext,
    result: IPluginExecutionResult,
  ): Promise<void> {
    const tokens = result.usage?.totalTokens ?? 0;
    console.log(`[MyPlugin] Run complete. Tokens used: ${tokens}`);
  }
 
  // Called on execution error
  async onError(error: Error, context?: IPluginErrorContext): Promise<void> {
    this.updateErrorStats();
    console.error(`[MyPlugin] Error in run: ${error.message}`);
  }
 
  getStats(): IMyPluginStats {
    return {
      ...super.getStats(),
      requestCount: this.stats.calls,
    };
  }
}

Register with Robota

import { Robota } from '@robota-sdk/agent-core';
import { AnthropicProvider } from '@robota-sdk/agent-provider/anthropic';
import { MyPlugin } from './my-plugin.js';
 
const agent = new Robota({
  name: 'MyAgent',
  aiProviders: [new AnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY })],
  defaultModel: { provider: 'anthropic', model: 'claude-sonnet-4-6' },
  plugins: [new MyPlugin()],
});

AbstractPlugin Lifecycle Hooks

Override any of these optional methods in your plugin:

// Before the agent processes a message
beforeExecution(context: IPluginExecutionContext): Promise<void> | void
 
// After successful completion
afterExecution(context: IPluginExecutionContext, result: IPluginExecutionResult): Promise<void> | void
 
// Before each tool call
beforeToolCall(toolName: string, parameters: TToolParameters): Promise<void> | void
 
// After each tool call
afterToolCall(toolName: string, parameters: TToolParameters, result: IToolExecutionResult): Promise<void> | void
 
// On any error
onError(error: Error, context?: IPluginErrorContext): Promise<void> | void
 
// Before the agent is disposed
cleanup(): Promise<void>

Built-in stats helpers

AbstractPlugin tracks basic stats automatically. Use these protected helpers and fields:

this.updateCallStats(); // increments this.stats.calls
this.updateErrorStats(); // increments this.stats.errors
this.stats.calls; // total calls
this.stats.errors; // total errors
this.stats.lastActivity; // Date of last call

EventEmitterPlugin — Event Subscriptions

For simpler use cases, subscribe to named events without subclassing:

import { EventEmitterPlugin, EVENT_EMITTER_EVENTS } from '@robota-sdk/agent-core';
 
const events = new EventEmitterPlugin();
 
// Subscribe to specific events
events.on(EVENT_EMITTER_EVENTS.EXECUTION_START, (data) => {
  console.log('Agent started run:', data.metadata?.executionId);
});
 
events.on(EVENT_EMITTER_EVENTS.TOOL_BEFORE_EXECUTE, (data) => {
  console.log(`Tool called: ${data.metadata?.toolName}`);
});
 
events.on(EVENT_EMITTER_EVENTS.EXECUTION_COMPLETE, (data) => {
  console.log('Run complete');
});
 
const agent = new Robota({
  // ...
  plugins: [events],
});

Available event constants

ConstantFired when
EXECUTION_STARTAgent begins processing a message
EXECUTION_COMPLETEAgent completes successfully
EXECUTION_ERRORAgent run fails
TOOL_BEFORE_EXECUTEA tool is about to be invoked
TOOL_AFTER_EXECUTEA tool has returned a result
TOOL_ERRORA tool call fails
CONVERSATION_STARTA conversation begins
CONVERSATION_COMPLETEA conversation ends
ERROR_OCCURREDAny error is logged

All constants are exported from EVENT_EMITTER_EVENTS in @robota-sdk/agent-core.


Example: Cost Tracking Plugin

import { AbstractPlugin, PluginCategory, PluginPriority } from '@robota-sdk/agent-core';
import type {
  IPluginExecutionContext,
  IPluginExecutionResult,
  IPluginStats,
} from '@robota-sdk/agent-core';
 
const COST_PER_1K_INPUT = 0.003; // USD per 1K input tokens
const COST_PER_1K_OUTPUT = 0.015; // USD per 1K output tokens
 
interface ICostStats extends IPluginStats {
  totalCostUsd: number;
}
 
export class CostTrackingPlugin extends AbstractPlugin<never, ICostStats> {
  name = 'CostTrackingPlugin';
  version = '1.0.0';
  category = PluginCategory.MONITORING;
  priority = PluginPriority.LOW;
 
  private totalCostUsd = 0;
 
  async afterExecution(
    _context: IPluginExecutionContext,
    result: IPluginExecutionResult,
  ): Promise<void> {
    const input = result.usage?.promptTokens ?? 0;
    const output = result.usage?.completionTokens ?? 0;
    const cost = (input / 1000) * COST_PER_1K_INPUT + (output / 1000) * COST_PER_1K_OUTPUT;
    this.totalCostUsd += cost;
  }
 
  getTotalCost(): number {
    return this.totalCostUsd;
  }
 
  getStats(): ICostStats {
    return {
      ...super.getStats(),
      totalCostUsd: this.totalCostUsd,
    };
  }
}

Example: Slack Notification Plugin

import { AbstractPlugin, PluginCategory, PluginPriority } from '@robota-sdk/agent-core';
import type { IPluginErrorContext } from '@robota-sdk/agent-core';
 
export class SlackNotificationPlugin extends AbstractPlugin {
  name = 'SlackNotificationPlugin';
  version = '1.0.0';
  category = PluginCategory.NOTIFICATION;
  priority = PluginPriority.LOW;
 
  constructor(private readonly webhookUrl: string) {
    super();
  }
 
  async onError(error: Error, _context?: IPluginErrorContext): Promise<void> {
    await fetch(this.webhookUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text: `Robota error: ${error.message}` }),
    });
  }
}

Publishing Your Plugin

Package naming

Community plugins should follow the naming convention:

@your-scope/robota-plugin-<name>
# Examples:
@your-org/robota-plugin-slack
@your-org/robota-plugin-linear
@your-org/robota-plugin-datadog

Minimal package.json

{
  "name": "@your-scope/robota-plugin-slack",
  "version": "1.0.0",
  "type": "module",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "peerDependencies": {
    "@robota-sdk/agent-core": ">=3.0.0"
  },
  "devDependencies": {
    "@robota-sdk/agent-core": "^3.0.0"
  }
}

Testing your plugin

import { describe, it, expect, vi } from 'vitest';
import { MyPlugin } from './my-plugin.js';
 
describe('MyPlugin', () => {
  it('increments call count on beforeExecution', async () => {
    const plugin = new MyPlugin();
    await plugin.initialize();
 
    await plugin.beforeExecution({ executionId: 'test-1' });
 
    expect(plugin.getStats().calls).toBe(1);
  });
 
  it('increments error count on onError', async () => {
    const plugin = new MyPlugin();
    await plugin.initialize();
 
    await plugin.onError(new Error('test'));
 
    expect(plugin.getStats().errors).toBe(1);
  });
});

Plugin Directory

Official plugins (maintained by the Robota team):

PluginImportDescription
EventEmitterPlugin@robota-sdk/agent-corePub/sub event subscriptions — subscribe to any lifecycle event

Community plugins: Submit yours to the plugin directory.