Skip to content

Plugin SDK

The OpenBot Plugin SDK (@meetopenbot/plugin-sdk) is the supported way to define plugins: event handlers, typed payloads, storage access, tool metadata, and client UI widgets. This page tracks the package API; for walk-throughs, see Your First Channel, Your First Agent, OpenBot.one client, Plugin Development, and Your First Plugin.

Terminal window
npm install @meetopenbot/plugin-sdk

The SDK declares peer dependencies on melony and zod. Ensure compatible versions are installed in your plugin project so TypeScript resolves types correctly (your package manager usually surfaces this when peers are missing).

The package root re-exports z from Zod alongside SDK modules, so you can import { z, definePlugin } from '@meetopenbot/plugin-sdk' when you want schema helpers without a separate Zod import.

Plugins run on OpenBot’s event bus Melony exposes to the host. You only need SDK types — you typically do not import Melony types directly in plugin code.

  1. Registration — The host loads your module and invokes factory(context) with a PluginContext.
  2. Subscriptionfactory returns a Melony plugin that uses the builder (PluginBuilder) to call builder.on(eventType, handler).
  3. Processing — Handlers run as async generators; they inspect the event and may call APIs or context.storage.
  4. Publication — Handlers yield further bus events (for example agent:output, client:ui:widget).

Use definePlugin for inference-friendly typing over a plain Plugin or PluginModule object.

Plugin is the shape the OpenBot host expects:

FieldPurpose
idStable plugin identifier.
name / descriptionHuman-readable metadata; image is optional branding.
configSchema?JSON-Schema-shaped description of plugin config validated from AGENT.md. See ConfigSchema.
toolDefinitions?Named tools merged for runtime plugins — see ToolDefinition.
factory(context: PluginContext) => PluginFactory — wires handlers onto the bus.

PluginModule is the same contract without id. Publish community plugins as npm packages: the host assigns id from the package name. Local / first-party plugins in AGENT.md typically use Plugin including id.

PluginRef (used in agent definitions) ties a plugin id to optional config:

interface PluginRef {
id: string;
config?: Record<string, unknown>;
}

When the host calls factory, it passes PluginContext:

FieldPurpose
agentIdAgent this plugin instance is wired for.
agentDetailsFull AgentDetails (Agent plus instructions and pluginRefs).
configEffective plugin configuration (from refs / host).
storageStorage facade for channels, threads, agents, variables, files, memories, specs, and persisted events.
toolsTool surface exposed to handlers (merged tool implementations; shape is host-dependent).

Runtime wiring (PluginFactory, PluginBuilder, PluginHandlerContext)

Section titled “Runtime wiring (PluginFactory, PluginBuilder, PluginHandlerContext)”

From the SDK’s perspective:

  • PluginFactory aliases MelonyPlugin — whatever you return from factory registers with Melony’s runtime.
  • PluginBuilder aliases MelonyBuilder — fluent API for builder.on(...).
  • PluginHandlerContext aliases Melony’s RuntimeContext — available inside handlers when the runtime provides it.

The host defines which Melony scheduler and state extensions are attached; handlers often receive OpenBot-aware state — see OpenBotState.

ConfigSchema describes AGENT.md plugin configuration: an object schema with typed properties (string | number | boolean | integer), optional enum, numeric bounds, descriptions, defaults, and format: 'password' \| 'url' \| 'email'.

ToolDefinition advertises tools for other plugins (for example LLM runtimes):

interface ToolDefinition {
description: string;
inputSchema: unknown;
}

toolDefinitions is a record of ToolDefinition keyed by tool name.

Plugins communicate by raising and handling events. All SDK events extend a BaseEvent: optional id, type string, optional meta, plus type-specific data.

Common correlation fields carried on meta (additional keys allowed):

FieldRole
agentId?Target or source agent where applicable.
threadId?Conversation thread.
toolCallId?Correlate tool calls and results.

In OpenBot, user-visible messaging is thread-scoped only: HTTP integrations must provide channelId and threadId together (Your First Channel). Types often keep threadId optional for portability — when handling agent:invoke for a normal chat turn, forward threadId into agentOutput / uiWidget so replies stay on the same thread.

Type stringTypeScript nameNotes
agent:invokeAgentInvokeEventUser / agent message ingress. data.content is the payload; optional role (user, assistant, system).
agent:outputAgentOutputEventAssistant-style reply to the client. meta.agentId is required.
client:ui:widgetUIWidgetEventRender a widget; data is RenderUIWidgetData (UI widgets). meta.agentId required.
client:ui:widget:responseUIWidgetResponseEventUser acted on a widget (widgetId, actionId, optional values / metadata).

Tool invocations use dynamic event types:

  • action:${toolName}ToolActionEvent: runtime requests a tool; data is tool-specific.
  • action:${toolName}:resultToolResultEvent: handler finished; data is the result payload.

Use toolResult() to construct a result event with consistent typing.

  • PluginEvent — Narrow union of invoke, output, widget, widget-response, and tool events above. The production bus may carry additional shapes; OpenBotEvent is PluginEvent or any BaseEvent with arbitrary data.
  • Handlers written against PluginEvent cover the documented authoring paths; fallback to OpenBotEvent when integrating experimental host events.

OpenBotState captures host-provided correlation on Melony handler contexts:

  • Identifiers: agentId, runId, channelId, threadId (conversation slices on OpenBot include threadId)
  • Hydrated snapshots: optional agentDetails, channelDetails, threadDetails
  • triggerEvent? — the bus event that started the slice of work (typed as OpenBotEvent)
  • shortTermMessages? — conversational scratch space as ShortTermMessage[] (system | user | assistant | tool variants; assistant entries may attach toolCalls, tool turns include toolCallId and toolName)

Exact population is host/runtime-dependent; guard optional fields before use.

HelperPurpose
shouldHandleInvoke(event, agentId)Returns whether this plugin instance should react to agent:invoke for the given agentId, respecting optional routing in event.data.agentId.
agentOutput({ agentId, content, threadId?, meta? })Builds a typed AgentOutputEvent.
toolResult(toolName, request, data)Builds action:${toolName}:result, forwarding meta from the originating request-like object.
uiWidget({ agentId, widget, threadId?, meta? })Builds client:ui:widget wrapping RenderUIWidgetData (see UI widgets).
withMeta(source, event)Merges source.meta into another event clone for correlation propagation.

Access persistence through PluginContext.storage.

Typed records include Agent, AgentDetails (extends Agent with instructions + pluginRefs), Channel, Thread, plus ChannelDetails and ThreadDetails (include spec and state blobs the host interprets).

Storage methods (all async):

Channels & threads

  • getChannels, createChannel, getChannelDetails
  • createThread, getThreads, getThreadDetails
  • patchChannelState, patchThreadState, patchChannelSpec, patchThreadSpec

Agents

  • getAgents, getAgentDetails, createAgent, updateAgent, deleteAgent

Events

  • getEvents — persisted bus history for scope (channelId; threadId required for thread transcripts in OpenBot)

Variables & files

  • getVariables, createVariable, deleteVariable
  • listFiles, readFile — workspace paths scoped by channelId

Memory

  • appendMemory, listMemories, deleteMemory, updateMemory
  • MemoryRecord: id, scope, content, optional tags[], timestamps
  • ListMemoriesArgs: filter by scopes, query, tag, limit

Refer to the SDK source storage.ts for exact argument and return types.

Widgets are described by UIWidgetSpec union members; when emitting you usually pass RenderUIWidgetData, which is the spec types with widgetId optional (the client may assign one).

All widgets extend UIWidgetBase:

  • widgetId, optional title / description
  • media?UIMediaItem (image | video | audio | file) with URLs and optional thumbnails
  • actions? — global UIWidgetAction buttons
  • state: open | submitted | cancelled | error
  • display: expanded | collapsed
  • size: small | medium | large | full
  • metadata bag for arbitrary client hints
kindExtra fields
messagebody? optional primary text alongside title/description
choiceactions required (at least one)
formfields: UIWidgetField[], optional submitLabel
listitems: UIWidgetListItem[]
mediaitems: UIMediaItem[]; layout?: single | grid | carousel

UIWidgetAction: id, label, optional value, variant?: 'primary' | 'secondary' | 'danger', disabled?

UIWidgetField (forms): id, label, type:

text | textarea | number | boolean | select | multiselect | date

plus optional description, placeholder, required, options[], defaultValue.

UIWidgetListItem: id, label, optional description, image, badge, actions[], status (pending | in_progress | done | error | cancelled), metadata.

import { definePlugin, shouldHandleInvoke, agentOutput } from '@meetopenbot/plugin-sdk';
export default definePlugin({
id: 'hello-world',
name: 'Hello World',
description: 'Responds with Hello World',
factory: (context) => (builder) => {
builder.on('agent:invoke', async function* (event) {
if (shouldHandleInvoke(event, context.agentId)) {
yield agentOutput({
agentId: context.agentId,
content: 'Hello, World!',
threadId: event.meta?.threadId,
});
}
});
},
});