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 serversThe 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 { Robota, type IAIProvider } from '@robota-sdk/agent-core';
import { LimitsPlugin, LoggingPlugin, UsagePlugin } from '@robota-sdk/agent-plugin';
 
declare const provider: IAIProvider;
 
const agent = new Robota({
  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.


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>
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 provider call and message
beforeProviderCall(messages: TUniversalMessage[]): Promise<void>
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 name of each tool that ran). The context carries executionId and the conversation messages.

IPluginHooks also declares beforeConversation, beforeToolCall, beforeToolExecution, afterToolCall and onStreamingChunk, but the Robota run loop does not call them. A plugin that needs per-tool or per-provider-call detail should use afterToolExecution or afterProviderCall. 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. Robota is on the 3.0.0 beta line; a caret range on a beta version (^3.0.0-beta.83) accepts later betas and the stable release:

{
  "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.0.0-beta.83"
  },
  "devDependencies": {
    "@robota-sdk/agent-core": "^3.0.0-beta.83"
  }
}

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, Robota } 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 Robota({
  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_AFTER_EXECUTEAfter a run, once per tool that ran (data.toolName)
TOOL_SUCCESSWith TOOL_AFTER_EXECUTE, for each tool call
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 server definitions
themes/Terminal themes

Plugins are published in marketplaces (a GitHub owner/repo or a git URL) and installed with /plugin, under ~/.robota/plugins/ for the user or .robota/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.