Skip to content
Robotadocs

Plugins

"Plugin" means two different things in Robota:

KindWhat it isWhere it runs
Runtime pluginA class that observes a Robota agent's lifecycle: runs, provider calls, messages, errorsYour code, through @robota-sdk/agent-core
CLI pluginA folder of skills, commands, agent definitions, hooks and MCP declarationsThe robota CLI, installed from a marketplace

Runtime plugins are for cross-cutting concerns in code you write: logging, usage and cost tracking, limits, notifications, audit trails. CLI plugins extend the robota assistant for its users. The rest of this page covers runtime plugins first, then CLI plugins.


Ready-made runtime plugins

@robota-sdk/agent-plugin ships eight plugins for the Robota class:

Plugin classConcern
ConversationHistoryPluginPersist and restore conversation history
ErrorHandlingPluginError classification, retries and recovery stats
ExecutionAnalyticsPluginPer-execution analytics
LimitsPluginToken, request and cost limits
LoggingPluginStructured logging of agent activity
PerformancePluginTiming and performance metrics
UsagePluginToken usage accounting
WebhookPluginSend lifecycle events to webhook endpoints
npm install @robota-sdk/agent-core @robota-sdk/agent-plugin

Plugins are passed to the Robota constructor in its plugins array:

import { ConversationAgent, type IAIProvider } from '@robota-sdk/agent-core';
import { LimitsPlugin, LoggingPlugin, UsagePlugin } from '@robota-sdk/agent-plugin';
 
declare const provider: IAIProvider;
 
const agent = new ConversationAgent({
  name: 'my-agent',
  aiProviders: [provider],
  defaultModel: { provider: 'anthropic', model: 'claude-sonnet-4-6' },
  plugins: [
    new LoggingPlugin({ strategy: 'console' }),
    new UsagePlugin({ strategy: 'memory' }),
    new LimitsPlugin({ strategy: 'token-bucket', maxTokens: 100_000 }),
  ],
});

Each plugin's options type is exported next to it (ILoggingPluginOptions, IUsagePluginOptions, ILimitsPluginOptions, IWebhookPluginOptions and so on), and most take a strategy that selects where data goes (for example 'console' | 'file' | 'remote' | 'silent' for logging). See the agent-plugin SPEC for each plugin's contract.

The main entry is platform-neutral, so what needs a Node host is passed in from @robota-sdk/agent-plugin/node. Writing to a file (storage: 'file' for ConversationHistoryPlugin, strategy: 'file' for LoggingPlugin and UsagePlugin) needs a filePath and a fileStore (new NodePluginTextFileStore()), and throws a ConfigurationError at construction without them. PerformancePlugin records memory and CPU figures only from a metricsCollector (new NodeSystemMetricsCollector()).


Writing a runtime plugin

A runtime plugin extends AbstractPlugin from @robota-sdk/agent-core and overrides the lifecycle methods it cares about. It needs a name and a version; category and priority classify it.

import {
  AbstractPlugin,
  PluginCategory,
  PluginPriority,
  type IPluginErrorContext,
  type IPluginExecutionContext,
  type IPluginExecutionResult,
  type IPluginOptions,
  type IPluginStats,
} from '@robota-sdk/agent-core';
 
interface IRunLoggerStats extends IPluginStats {
  completedRuns: number;
}
 
export class RunLoggerPlugin extends AbstractPlugin<IPluginOptions, IRunLoggerStats> {
  name = 'RunLoggerPlugin';
  version = '1.0.0';
  private completedRuns = 0;
 
  constructor() {
    super();
    this.category = PluginCategory.MONITORING;
    this.priority = PluginPriority.NORMAL;
  }
 
  // Before the agent starts working on a message.
  override async beforeExecution(context: IPluginExecutionContext): Promise<void> {
    this.updateCallStats();
    console.log(`[RunLogger] start ${context.executionId ?? ''}`);
  }
 
  // After the run completes.
  override async afterExecution(
    context: IPluginExecutionContext,
    result: IPluginExecutionResult,
  ): Promise<void> {
    this.completedRuns += 1;
    console.log(
      `[RunLogger] done ${context.executionId ?? ''} in ${result.duration ?? 0} ms, ` +
        `${result.toolsExecuted ?? 0} tool calls`,
    );
  }
 
  // When the run fails.
  override async onError(error: Error, context?: IPluginErrorContext): Promise<void> {
    this.updateErrorStats();
    console.error(`[RunLogger] ${context?.executionId ?? ''} failed: ${error.message}`);
  }
 
  override getStats(): IRunLoggerStats {
    return { ...super.getStats(), completedRuns: this.completedRuns };
  }
}

Register it like any other plugin: plugins: [new RunLoggerPlugin()].

Lifecycle methods the agent calls

// Once per run
beforeRun(input: string, options?: IRunOptions): Promise<void>
beforeExecution(context: IPluginExecutionContext): Promise<void>
beforeConversation(context: IPluginExecutionContext): Promise<void>
afterRun(input: string, response: string, options?: IRunOptions): Promise<void>
afterExecution(context: IPluginExecutionContext, result: IPluginExecutionResult): Promise<void>
afterConversation(context: IPluginExecutionContext, result: IPluginExecutionResult): Promise<void>
afterToolExecution(context: IPluginExecutionContext, result: IPluginExecutionResult): Promise<void> // only when tools ran
 
// Around every tool call
beforeToolCall(toolName: string, parameters: TToolParameters, context?: IToolExecutionContext): Promise<void>
beforeToolExecution(context: IPluginExecutionContext, toolData: IToolExecutionContext): Promise<void>
afterToolCall(toolName: string, parameters: TToolParameters, result: IToolExecutionResult): Promise<void>
 
// Around every provider call and message
beforeProviderCall(messages: TUniversalMessage[]): Promise<void>
onStreamingChunk(chunk: TUniversalMessage): Promise<void> // each streamed piece of text, in order
afterProviderCall(messages: TUniversalMessage[], response: TUniversalMessage): Promise<void>
onMessageAdded(message: TUniversalMessage): Promise<void>
 
// When a run fails
onError(error: Error, context?: IPluginErrorContext): Promise<void>

The result given to the after-run methods carries response, duration, tokensUsed (when the provider reported usage), toolsExecuted, success, and toolCalls (the id and name of each tool call that ran, with result: null for one that failed). The context carries executionId and the conversation messages.

beforeToolCall and beforeToolExecution run just before each tool call is dispatched, and afterToolCall runs when it returns, with its IToolExecutionResult. afterToolExecution runs once after the run, with the toolCalls of the whole run. A plugin method that throws is logged and does not stop the run.

Stats helpers

AbstractPlugin counts calls and errors for getStats(). Inside a plugin:

this.updateCallStats(); // increments calls and records the time
this.updateErrorStats(); // increments errors and records the time

getStats() returns enabled, calls, errors, moduleEventsReceived and lastActivity; override it to add your own fields. enable(), disable(), getStatus() and dispose() are available on every plugin.

Example: cost tracking

afterProviderCall sees every response message, and readTokenUsageFromMessage reads its usage metadata:

import {
  AbstractPlugin,
  PluginCategory,
  readTokenUsageFromMessage,
  type TUniversalMessage,
} from '@robota-sdk/agent-core';
 
const USD_PER_1K_INPUT = 0.003;
const USD_PER_1K_OUTPUT = 0.015;
 
export class CostTrackingPlugin extends AbstractPlugin {
  name = 'CostTrackingPlugin';
  version = '1.0.0';
  private totalCostUsd = 0;
 
  constructor() {
    super();
    this.category = PluginCategory.MONITORING;
  }
 
  override async afterProviderCall(
    _messages: TUniversalMessage[],
    response: TUniversalMessage,
  ): Promise<void> {
    const usage = readTokenUsageFromMessage(response);
    if (usage === undefined) return;
    this.totalCostUsd +=
      (usage.inputTokens / 1000) * USD_PER_1K_INPUT +
      (usage.outputTokens / 1000) * USD_PER_1K_OUTPUT;
  }
 
  getTotalCost(): number {
    return this.totalCostUsd;
  }
}

@robota-sdk/agent-core also exports calculateModelCost and lookupModelPrice if you would rather use Robota's own model price table.

Example: error notification

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

For more than a single notification, WebhookPlugin in @robota-sdk/agent-plugin sends lifecycle events to several endpoints with retries.

Testing a runtime plugin

Lifecycle methods are ordinary async methods, so a test can call them directly:

import { describe, expect, it } from 'vitest';
import { RunLoggerPlugin } from './run-logger-plugin.js';
 
describe('RunLoggerPlugin', () => {
  it('counts a completed run', async () => {
    const plugin = new RunLoggerPlugin();
    await plugin.beforeExecution({ executionId: 'test-1' });
    await plugin.afterExecution({ executionId: 'test-1' }, { duration: 5, toolsExecuted: 0 });
 
    expect(plugin.getStats().calls).toBe(1);
    expect(plugin.getStats().completedRuns).toBe(1);
  });
});

Publishing a runtime plugin

A plugin package needs only @robota-sdk/agent-core. Declare it as a peer dependency so your plugin uses the application's copy:

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

Subscribing to events: EventEmitterPlugin

EventEmitterPlugin (in @robota-sdk/agent-core) turns the lifecycle into named events you can subscribe to without writing a class:

import {
  EVENT_EMITTER_EVENTS,
  EventEmitterPlugin,
  ConversationAgent,
} from '@robota-sdk/agent-core';
import type { IAIProvider } from '@robota-sdk/agent-core';
 
declare const provider: IAIProvider;
 
const events = new EventEmitterPlugin();
 
events.on(EVENT_EMITTER_EVENTS.AGENT_EXECUTION_START, (event) => {
  console.log('Run started:', event.executionId);
});
 
events.on(EVENT_EMITTER_EVENTS.TOOL_AFTER_EXECUTE, (event) => {
  console.log('Tool ran:', event.data?.['toolName']);
});
 
events.on(EVENT_EMITTER_EVENTS.AGENT_EXECUTION_COMPLETE, (event) => {
  console.log('Run complete in', event.data?.['duration'], 'ms');
});
 
const agent = new ConversationAgent({
  name: 'EventAgent',
  aiProviders: [provider],
  defaultModel: { provider: 'anthropic', model: 'claude-sonnet-4-6' },
  plugins: [events],
});

Each listener receives an IEventEmitterEventData: type, timestamp, executionId, sessionId, userId, data, error and metadata. A Robota run emits these events through the plugin:

Event constantEmitted
AGENT_EXECUTION_STARTWhen a run starts
AGENT_EXECUTION_COMPLETEWhen a run completes (data.duration, data.tokensUsed, data.toolsExecuted)
AGENT_EXECUTION_ERRORWhen a run fails (error)
TOOL_BEFORE_EXECUTEJust before each tool call is dispatched (data.toolName, data.toolId, data.arguments)
TOOL_AFTER_EXECUTEAfter a run, once per tool call that ran (data.toolName)
TOOL_SUCCESSWith TOOL_AFTER_EXECUTE, for each tool call that succeeded
TOOL_ERRORWith TOOL_AFTER_EXECUTE, for each tool call that failed
CONVERSATION_STARTWhen a run starts; only if listed in the events option
CONVERSATION_COMPLETEWhen a run completes; only if listed in the events option

By default the plugin emits only the agent-execution and tool events; pass new EventEmitterPlugin({ events: [...] }) to choose the list. Other options include filters (per-event predicates), buffer (batch delivery) and async.


CLI plugins

A robota CLI plugin is a folder with a manifest at .claude-plugin/plugin.json (name, version, description, features), the same layout Claude Code plugins use:

Path in the pluginContributes
skills/<name>/SKILL.mdSkills, shown as /<name> with the plugin's name as a hint
commands/<name>.mdCommands, shown as /<plugin>:<name>
agents/Agent definitions for subagents
hooks/hooks.jsonLifecycle hooks, merged with the session's own
.mcp.jsonMCP declarations for inspection; configure runtime servers in settings
themes/Terminal themes

Plugins are published in marketplaces (a GitHub owner/repo or a git URL) and installed with /plugin, under <user-state>/plugins/ for the user or <project-state>/plugins/ for a project. See CLI Reference — Plugins for the commands, and Permissions and Hooks — Plugin Hooks for the environment a plugin hook runs with.


Plugin .mcp.json declarations are inspected by /doctor but are not activated as MCP sources. Copy a supported server definition into mcpServers in your settings and approve it with /mcp approve <name>. The stock CLI supports approved public Streamable HTTP servers; starting stdio programs requires host-provided authority, and private-network endpoints remain blocked.