Plugin Authoring Guide
July 26, 2026 · View on GitHub
This guide explains the plugin system as it exists today in Molibot:
- what kinds of plugins exist
- how to write a plugin
- how to install and enable a plugin
- what format and structure a plugin should follow
- a full demo using the built-in Cloudflare HTML publish plugin
Important: this guide describes the current real behavior, not the future ideal state.
1. Current support matrix
Molibot has four practical plugin directions:
| Type | What it is for | Can it run today? | How it is added today? |
|---|---|---|---|
| pi extension | Third-party extension from the pi ecosystem: agent tools, runtime event handlers, slash commands | Yes, installed at runtime | Install from npm or git in /settings/plugins, or drop a directory into ${DATA_DIR}/extensions/ |
| Channel plugin | New inbound/outbound chat channels such as Telegram, Feishu, QQ, Weixin | Yes, if built into the codebase | Add code and register it in the built-in channel registry |
| Feature plugin | New product capability for the agent, settings, and prompt, such as HTML publishing | Yes, if built into the codebase | Add code and register it in the built-in feature registry |
| Provider plugin | New model/provider integration | Built-in only | External manifests can be discovered, but external provider execution is not wired into runtime yet |
There is also a separate external plugin manifest scan:
${DATA_DIR}/plugins/channels/*/plugin.json${DATA_DIR}/plugins/providers/*/plugin.json
That external scan is useful today for discovery and catalog display only: those manifests are not executed. The runtime-executed external path is pi extensions, described next.
2. The most important distinction
If someone asks “how do I install a plugin into Molibot?”, there are three answers:
2.1 Third-party pi extension (the runtime-installable path)
Molibot reuses pi's extension loader, so an existing pi extension can be installed and used without touching this repository. There are two ways in.
From the settings page: open /settings/plugins → pi extensions, paste
whatever link you have into the single field, press Install. The extension is
staged, validated (it must load and register something), then moved into
${DATA_DIR}/extensions/<id>/ and loaded — no restart.
The field accepts any of these; the source is detected, not chosen:
| Input | Result |
|---|---|
pi-subagents, @scope/name, name@1.2.3 | npm install |
https://www.npmjs.com/package/<name> (with or without /v/<version>) | npm install |
https://github.com/owner/repo (.git optional) | git clone |
https://github.com/owner/repo/tree/<branch>/<path> | git clone of that branch, then only that subdirectory — the common shape for pi extensions living in a monorepo |
https://gitlab.com/group/repo/-/tree/<branch>/<path> | same |
git@host:owner/repo.git | git clone over SSH |
file:///path/to/repo | local clone, for an extension you are writing yourself |
A link that cannot be understood is refused up front with a hint, instead of
failing later as a raw git clone error.
From a chat: ask the agent — "install this plugin: <link>". That runs the
extensionManage tool, which can also list what is installed, inspect a link
without installing it, uninstall, and enable/disable. Installing downloads
and executes third-party code, so it is classified critical risk and always
raises an approval card first; approve it the same way as a Host Bash request
(button, or a plain 同意 / approve reply on channels without buttons). That
gate is not about distrusting the owner: "install this plugin" is a sentence that
can appear in a page or document the agent happened to read, and only the owner
can confirm they actually asked for it.
Discovery follows pi's own rules, so a manually placed directory works too:
${DATA_DIR}/extensions/<name>/index.tsorindex.js${DATA_DIR}/extensions/<name>/package.jsonwith api.extensionsentry list${DATA_DIR}/extensions/<name>.ts(single file)
What works: registerTool, registerCommand, registerFlag, and event
handlers for agent_start, agent_end, tool_call (including blocking a call
and patching arguments in place), tool_result, input, before_agent_start
and session_start.
What does not work, because Molibot is a multi-channel server with no
terminal: registerShortcut, registerMessageRenderer / registerEntryRenderer,
every ctx.ui dialog and widget method, ctx.sessionManager and
ctx.modelRegistry (pi-specific objects Molibot does not have), the session-tree
events (session_before_fork / switch / compact / tree), user_bash,
message_update, before_provider_request / before_provider_headers,
resources_discover and model_select. Dialog calls return "no answer" instead
of blocking; the settings page flags an extension that registered anything from
this list.
Scope: a master switch plus a per-extension switch, and each extension can be turned off for individual bots.
2.2 Built-in plugin (channels, features, providers)
If the capability belongs to Molibot itself rather than to a third party:
- add plugin code into this repository
- register it in a built-in registry
- add any required settings fields
- expose its settings UI if needed
- restart the app
This is the path used by the current Cloudflare HTML publish plugin.
2.3 External channel/provider manifest
If someone creates a folder under ${DATA_DIR}/plugins/... with a plugin.json, Molibot can currently:
- discover it
- validate the manifest
- show it in
/settings/plugins
But Molibot will not execute that external plugin module. For a third-party capability that must actually run, use a pi extension (§2.1).
3. Plugin types in plain English
3.1 Channel plugin
A channel plugin connects Molibot to a messaging platform.
Examples:
- Telegram
- Feishu
- Weixin
It handles:
- receiving messages
- normalizing them
- sending replies back
- starting and stopping cleanly
3.2 Feature plugin
A feature plugin adds a product capability to the runtime itself.
Examples:
- publish HTML to R2
- export generated files somewhere
- post generated content to an internal service
- register a special tool the agent can call
A feature plugin can do any of these:
- expose settings
- inject instructions into the system prompt
- register one or more agent tools
- validate whether it is enabled and configured
This is the best fit for “generate HTML, upload it, then return a URL”.
3.3 Provider plugin
A provider plugin would extend model/provider integrations.
Today, built-in providers work. External provider manifests are discoverable, but not executable yet.
4. How built-in feature plugins work
Today’s built-in feature plugins are registered in:
Each built-in feature plugin currently has four practical parts:
- identity
- enabled check
- settings field declaration
- prompt guidance
- tool registration
The effective shape is:
interface BuiltInFeaturePlugin {
key: string;
name: string;
version?: string;
description?: string;
settingsKey: keyof RuntimeSettings["plugins"];
settingsFields?: PluginSettingField[];
isEnabled: (settings: RuntimeSettings) => boolean;
buildPromptSection?: (settings: RuntimeSettings) => string | null;
createTools?: (context: { getSettings: () => RuntimeSettings }) => AgentTool<any>[];
}
In other words:
isEnableddecides whether the plugin is activesettingsFieldstells/settings/pluginswhat form controls to renderbuildPromptSectiontells the agent what this plugin doescreateToolsgives the agent the actual action surface
5. What files you usually need when writing a built-in feature plugin
For a real usable built-in feature plugin, you will usually touch these places:
5.1 Plugin runtime registration
src/lib/server/plugins/feature-registry.tssrc/lib/server/plugins/<your-plugin>/plugin.ts
Plugin-specific declaration should live inside the plugin's own subdirectory. The root feature registry should stay as a thin aggregator that imports built-in plugins and exposes the combined catalog/prompt/tool entry points.
5.2 Settings schema
src/lib/server/settings/schema.ts
This defines what fields the plugin needs.
5.3 Settings defaults
src/lib/server/settings/defaults.ts
This defines default values and optional env fallbacks.
5.4 Settings persistence
src/lib/server/settings/store.tssrc/lib/server/app/runtime.tssrc/routes/api/settings/+server.ts
These make sure plugin settings can be saved, loaded, validated, and merged safely.
5.5 Settings UI
src/routes/settings/plugins/+page.svelte
This is where the operator turns the plugin on and fills in its config.
Important: first-class feature plugins should not require hand-written per-plugin HTML in the settings page. The plugin should declare its own settings fields in the registry metadata, and the settings page should render those fields dynamically.
5.6 Agent prompt injection
src/lib/server/agent/prompt.ts
This is where the plugin explains itself to the agent.
5.7 Plugin-owned tool implementation
src/lib/server/plugins/<your-plugin>/...src/lib/server/plugins/feature-registry.ts
The plugin should own its own runtime actions and plugin declaration inside its own subdirectory. The outer agent/runtime layer should load those actions from the root feature registry instead of keeping plugin-specific code under the generic agent tools directory.
6. Built-in feature plugin template
This is the smallest realistic template for a new built-in feature plugin.
6.1 Registry template
import type { AgentTool } from "@mariozechner/pi-agent-core";
import type { RuntimeSettings } from "../settings/index.js";
interface BuiltInFeaturePlugin {
key: string;
name: string;
version?: string;
description?: string;
settingsKey: keyof RuntimeSettings["plugins"];
settingsFields?: PluginSettingField[];
isEnabled: (settings: RuntimeSettings) => boolean;
buildPromptSection?: (settings: RuntimeSettings) => string | null;
createTools?: (context: { getSettings: () => RuntimeSettings }) => AgentTool<any>[];
}
const exampleFeaturePlugin: BuiltInFeaturePlugin = {
key: "example-feature",
name: "Example Feature",
version: "built-in",
description: "Short description of what this plugin adds.",
settingsKey: "exampleFeature",
settingsFields: [
{ key: "enabled", label: "Enable Example Feature", type: "boolean", defaultValue: false },
{ key: "apiBaseUrl", label: "API base URL", type: "text", required: true, placeholder: "https://example.com" },
{ key: "apiKey", label: "API key", type: "password", required: true }
],
isEnabled: (settings) => settings.plugins.exampleFeature.enabled,
buildPromptSection: (settings) => {
if (!settings.plugins.exampleFeature.enabled) return null;
return [
"## Installed Feature Plugin: Example Feature",
"- Explain to the agent when to use it.",
"- Explain what it can do.",
"- Explain what it must never fake."
].join("\\n");
},
createTools: (context) => {
if (!context.getSettings().plugins.exampleFeature.enabled) return [];
return [
// return one or more AgentTool instances here
];
}
};
6.2 Settings template
Add a new block under plugins in your runtime settings schema:
export interface ExampleFeaturePluginSettings {
enabled: boolean;
apiBaseUrl: string;
apiKey: string;
}
export interface PluginSettings {
memory: MemoryBackendSettings;
exampleFeature: ExampleFeaturePluginSettings;
}
6.3 Prompt template
Good plugin prompt text should always cover:
- when the agent should use the plugin
- what counts as valid input
- what success looks like
- what not to fake
- what to say when it fails
A good pattern:
## Installed Feature Plugin: Example Feature
- When the user asks for X, call `example_tool`.
- Only do this when input includes A and B.
- Success returns C.
- Never invent a result.
- If it fails, say it failed and report the actual error.
7. How to install a usable plugin today
Today, “installing a plugin” usually means “adding a built-in plugin into this repo”.
7.1 Steps
- Add the plugin runtime code.
- Register it in the correct built-in registry.
- Add its config fields to settings schema/defaults/store/runtime validation.
- Add its UI to
/settings/pluginsif it needs operator config. - Restart Molibot.
7.2 What “enabled” means
A plugin being visible in the catalog is not enough.
For a feature plugin to really work:
- it must be registered in code
- it must be enabled in settings
- it must be fully configured if it depends on credentials or URLs
8. How to enable a built-in feature plugin
For the current first feature plugin:
- Open
/settings/plugins - Find
Cloudflare HTML Publish - Turn it on
- Fill in:
- Public base URL
- Route prefix
- Bucket name
- Account ID
- Access Key ID
- Secret Access Key
- R2 object prefix
- Save
Once saved:
- the plugin appears as enabled in the feature plugin catalog
- the agent receives plugin guidance in its system prompt
- the
publish_htmltool becomes available when the plugin is fully configured
9. External plugin manifest format today
External manifest support exists today only for discovery.
9.1 Directory layout
${DATA_DIR}/plugins/
channels/
my-channel/
plugin.json
index.js
providers/
my-provider/
plugin.json
index.js
9.2 Manifest shape
{
"kind": "channel",
"key": "my-channel",
"name": "My Channel",
"version": "0.1.0",
"description": "Optional description",
"entry": "./index.js"
}
9.3 What this gets you today
If the manifest is valid, Molibot can:
- detect it
- list it in
/settings/plugins - show manifest or entry-file errors
It does not yet execute that external module.
10. Cloudflare HTML publish plugin demo
This plugin is the reference demo for the first built-in feature plugin path.
10.1 What it does
It lets the agent:
- receive a complete HTML document
- upload it to Cloudflare R2
- generate a random
.htmlfile name - return a public URL
Example final URL shape:
https://example.com/html/ab12cd34ef56gh78ij90.html
10.2 What it does not do
It does not:
- configure your Cloudflare Pages project
- create and deploy your Worker for you automatically
- manage your Cloudflare domain routing for you
- publish partial HTML fragments
It only handles the Molibot side:
- save config
- expose agent instructions
- upload HTML to R2
- return the final public link
This plugin directory now also includes a Worker template you can deploy manually:
src/lib/server/plugins/cloudflareHtml/worker/index.jssrc/lib/server/plugins/cloudflareHtml/worker/module.tssrc/lib/server/plugins/cloudflareHtml/worker/wrangler.example.toml
10.3 Actual config fields
Current settings are stored under:
plugins.cloudflareHtml
Fields:
{
enabled: boolean;
accessMode: "worker" | "direct";
workerBaseHost: string;
publicBaseHost: string;
routePrefix: string;
bucketName: string;
accountId: string;
accessKeyId: string;
secretAccessKey: string;
objectPrefix: string;
}
10.4 Agent tool surface
The plugin currently exposes one tool:
publish_html({
html: string,
title?: string
})
Rules:
htmlmust be a complete document- it must include
<html>,<head>, and<body> - output file name is generated automatically
- on success, the tool returns the final public URL
- on failure, it returns the real upload error
- the public URL can be built either through Worker mode or Direct R2 mode, depending on plugin settings
10.5 Demo prompt
This is the kind of user request that should work well with the plugin:
帮我做一个完整的落地页 HTML,主题是极简咖啡品牌,做完后直接发布并把链接给我。
Expected flow:
- agent generates a full HTML document
- plugin guidance reminds the agent that HTML publishing is available
- agent calls
publish_html - HTML is uploaded into the configured R2 prefix
- agent returns the public link
10.6 Demo object-path result
If the plugin is configured like this:
accessMode = worker
workerBaseHost = https://example.com
routePrefix = /html
objectPrefix = html/
Then a successful publish might become:
R2 object key: html/ab12cd34ef56gh78ij90.html
Public URL: https://example.com/html/ab12cd34ef56gh78ij90.html
Direct R2 mode example:
accessMode = direct
publicBaseHost = https://pub-xxxxxxxx.r2.dev
objectPrefix = html/
Then a successful publish might become:
R2 object key: html/ab12cd34ef56gh78ij90.html
Public URL: https://pub-xxxxxxxx.r2.dev/html/ab12cd34ef56gh78ij90.html
11. Recommended authoring rules
When writing new plugins for Molibot, follow these rules:
11.1 Keep the plugin single-purpose
A plugin should do one clear thing well.
Good:
- publish HTML
- export reports
- post to one internal service
Bad:
- upload HTML
- configure DNS
- rewrite the prompt
- create tasks
- deploy infrastructure
all inside one plugin
11.2 Be honest with status
Never document a plugin as installable if it is only discoverable.
11.3 Put operator config in one place
If a plugin needs setup, put it under /settings/plugins unless there is a very strong reason not to.
11.4 Tell the agent what not to fake
This matters a lot.
A good plugin prompt always says:
- what success is
- what failure is
- that URLs/results must not be invented
11.5 Validate before exposing tools
If a plugin depends on credentials, URLs, or external services, do not expose its runtime tool until configuration is complete.
That is how the current Cloudflare HTML plugin works.
12. Short answer: how someone should write a plugin today
If you want a plugin that really works today:
- write it as a built-in plugin in this repo
- register it in code
- add settings + UI
- add prompt guidance
- add one or more agent tools
If you only want it to appear in the plugin catalog:
- create a
plugin.json - place it under
${DATA_DIR}/plugins/channels/...or${DATA_DIR}/plugins/providers/... - Molibot will discover it
But that second path is not enough to make it run.