Plugin SDK

Extend Iron Rain with custom hooks that run before, during, and after task dispatch.

Installation

npm install @howlerops/iron-rain-plugin

The plugin package re-exports all core types, so you don't need to install @howlerops/iron-rain separately.

Quick example

import { definePlugin } from '@howlerops/iron-rain-plugin';

export default definePlugin({
  name: 'my-logger',
  version: '1.0.0',
  description: 'Logs every dispatch to the console',

  hooks: {
    onInit(ctx) {
      console.log('Logger plugin loaded');
    },

    onBeforeDispatch(task, ctx) {
      console.log(`Dispatching: ${task.prompt.slice(0, 50)}...`);
      return task;  // pass through unchanged
    },

    onAfterDispatch(episode, ctx) {
      console.log(
        `[${episode.slot}] ${episode.status} — ` +
        `${episode.tokens} tokens, ${episode.duration}ms`
      );
    }
  }
});

Plugin definition

interface PluginDefinition {
  name: string;          // Unique plugin name
  version: string;       // Semver version
  description?: string;  // Human-readable description
  hooks: PluginHooks;     // Lifecycle hooks
}

The definePlugin() function is an identity function used for TypeScript type inference. Wrap your plugin in it for full autocompletion.

Lifecycle hooks

onInit(ctx)

Called once when the plugin is loaded. Use it for setup, connections, or logging.

onInit(ctx: PluginContext): void | Promise<void>;

onBeforeDispatch(task, ctx)

Called before every task is dispatched to a slot worker. You can inspect or mutate the task.

onBeforeDispatch(
  task: OrchestratorTask,
  ctx: PluginContext
): OrchestratorTask | void;

Example: add system prompt to all tasks

onBeforeDispatch(task, ctx) {
  return {
    ...task,
    metadata: {
      ...task.metadata,
      systemPrompt: 'Always respond in JSON format'
    }
  };
}

onAfterDispatch(episode, ctx)

Called after every dispatch completes. The episode is read-only — use this for logging, analytics, or side effects.

onAfterDispatch(
  episode: EpisodeSummary,
  ctx: PluginContext
): void;

Example: track costs

let totalTokens = 0;

onAfterDispatch(episode, ctx) {
  totalTokens += episode.tokens;
  console.log(`Total tokens used: ${totalTokens}`);
}

onSlotChange(slot, config, ctx)

Called when a slot's model configuration is updated at runtime.

onSlotChange(
  slot: SlotName,
  config: SlotConfig,
  ctx: PluginContext
): void;

onDestroy(ctx)

Called on shutdown. Use for cleanup — close connections, flush buffers, etc.

onDestroy(ctx: PluginContext): void | Promise<void>;

Plugin context

Every hook receives a PluginContext with access to the running system:

interface PluginContext {
  config: IronRainConfig;                          // Current config
  getSlot(name: SlotName): SlotConfig;              // Read a slot
  dispatch(task: OrchestratorTask): Promise<EpisodeSummary>;  // Dispatch
}
Avoid infinite loops

Calling ctx.dispatch() inside onBeforeDispatch or onAfterDispatch can create infinite recursion. Only dispatch from onInit or from external triggers.

TypeScript setup

The plugin SDK exports all the types you need:

import type {
  PluginDefinition,
  PluginHooks,
  PluginContext,
  OrchestratorTask,
  EpisodeSummary,
  SlotName,
  SlotConfig,
  IronRainConfig,
  ChatMessage,
  MessageContent,
  ResolvedReference
} from '@howlerops/iron-rain-plugin';

The core package also exports context reference utilities:

import {
  parseReferences,
  getTextContent
} from '@howlerops/iron-rain';

import type {
  ResolvedReference,
  ParsedInput,
  ImageContent,
  TextContent
} from '@howlerops/iron-rain';