create-donobu-plugin
v1.7.0
Published
Create a new Donobu plugin
Readme
create-donobu-plugin
create-donobu-plugin is the official scaffolding CLI for Donobu Studio plugins. It generates a TypeScript project wired to Donobu's plugin API and pins dependencies to whatever versions your local Donobu installation already uses.
How Donobu loads plugins
Donobu watches a plugins directory inside its working data folder:
| Platform | Path |
| -------- | ------------------------------------------------------ |
| macOS | ~/Library/Application Support/Donobu Studio/plugins/ |
| Windows | %APPDATA%/Donobu Studio/plugins/ |
| Linux | ~/.config/Donobu Studio/plugins/ |
Each plugin ships a bundled dist/index.mjs. On startup Donobu imports every plugin and calls the exported functions it finds:
loadCustomTools(deps)— returns an array ofToolinstances.loadPersistencePlugin(deps)— returns{ key, plugin }to register a persistence backend, ornullto skip.
A single plugin may export both functions. Scoped package names (e.g., @myorg/my-plugin) are supported.
npm exec install-donobu-plugin builds your project and copies dist/ into the plugins directory. Restarting Donobu is enough to pick up changes.
Prerequisites
- Node.js 20+ and a package manager (npm 8+, pnpm 10+, or yarn)
- A local Donobu Studio installation (desktop or backend) so the installer has somewhere to copy the plugin bundle
- Write access to the Donobu Studio working directory
Quick start
Tool plugin (default)
npx create-donobu-plugin my-support-tools
cd my-support-tools && npm install
# Edit src/index.ts to add your custom tools
npm run build
npm exec install-donobu-plugin
# Restart Donobu StudioPersistence plugin
npx create-donobu-plugin my-storage --type persistence
cd my-storage && npm install
# Edit src/index.ts to implement your persistence plugin
npm run build
npm exec install-donobu-plugin
# Set PERSISTENCE_PRIORITY to include your plugin key, then restart DonobuPlugin names may include lowercase letters, numbers, hyphens, and underscores. The CLI prints usage information if the argument is missing or invalid.
Generated project layout
| Item | Description |
| -------------------- | ----------------------------------------------------------------------------------------------- |
| package.json | Private package with "donobu-plugin": true. donobu is a peer/dev dependency, never bundled. |
| tsconfig.json | ES2021 + NodeNext compiler settings that emit declarations into dist/. |
| esbuild.config.mjs | Bundles TypeScript source into dist/index.mjs, keeping donobu external. |
| src/index.ts | Entry point. Exports loadCustomTools(deps) and/or loadPersistencePlugin(deps). |
| README.md | Template instructions for the team that owns the generated plugin. |
The deps pattern
Plugins receive all runtime values through the deps parameter — never use value imports from donobu. Only import type is allowed because type imports are erased at compile time, while value imports would produce an import ... from "donobu" statement in the bundle that Node cannot resolve from the plugins directory.
// CORRECT — type imports are erased, runtime values come from deps
import type { Tool, PluginDependencies } from 'donobu';
export async function loadCustomTools(
deps: PluginDependencies,
): Promise<Tool<any, any>[]> {
const logger = deps.donobu.appLogger; // runtime access via deps
// ...
}// WRONG — this creates a runtime import that will fail at load time
import { appLogger } from 'donobu';What is available on deps
deps.donobu— the full public SDK:createTool,Tool,ReplayableInteraction,appLogger,MiscUtils,PlaywrightUtils, schemas, exception classes, etc.
Feel free to add your own npm dependencies; esbuild bundles everything except the packages listed in external.
Authoring tool plugins
Every tool plugin exports loadCustomTools(deps) and returns an array of Tool instances. The simplest way to create a tool is deps.donobu.createTool():
import type { Tool, PluginDependencies } from 'donobu';
import { z } from 'zod/v4';
export async function loadCustomTools(
deps: PluginDependencies,
): Promise<Tool<any, any>[]> {
return [
deps.donobu.createTool(deps, {
name: 'judgeWebpageTitle',
description: 'Judge the quality of a title to a webpage.',
schema: z.object({ webpageTitle: z.string() }),
requiresGpt: true,
call: async (context, parameters) => {
const resp = await context.gptClient!.getMessage([
{
type: 'user',
items: [
{
type: 'text',
text: `Judge the quality of this webpage title on a scale of 1 (worst) to 10 (best) and explain why.
Title: ${parameters.webpageTitle}`,
},
],
},
]);
return {
isSuccessful: true,
forLlm: resp.text,
metadata: null,
};
},
}),
];
}For tools that need to extend ReplayableInteraction or another base class, use a factory function so the base class comes from deps.donobu rather than a value import:
import type { Tool, PluginDependencies } from 'donobu';
function createMyTool(deps: PluginDependencies): Tool<any, any> {
const { ReplayableInteraction } = deps.donobu;
class MyTool extends ReplayableInteraction</* ... */> {
// ...
}
return new MyTool();
}
export async function loadCustomTools(deps: PluginDependencies) {
return [createMyTool(deps)];
}Authoring persistence plugins
Persistence plugins export loadPersistencePlugin(deps). The returned key must appear in the PERSISTENCE_PRIORITY environment variable for Donobu to use it.
import type {
EnvPersistence,
FlowsPersistence,
PersistencePlugin,
PluginDependencies,
} from 'donobu';
export async function loadPersistencePlugin(
deps: PluginDependencies,
): Promise<{ key: string; plugin: PersistencePlugin } | null> {
const bucket = process.env.MY_STORAGE_BUCKET;
if (!bucket) return null; // skip registration if config is missing
return {
key: 'MY_CUSTOM',
plugin: {
async createFlowsPersistence(): Promise<FlowsPersistence | null> {
// Return your FlowsPersistence implementation, or null.
return null;
},
async createEnvPersistence(): Promise<EnvPersistence | null> {
// Return your EnvPersistence implementation, or null.
return null;
},
},
};
}If your persistence classes need SDK utilities like appLogger or FlowNotFoundException, accept deps.donobu as a constructor parameter rather than importing them directly.
Build and install
npm run build
npm exec install-donobu-pluginnpm run build cleans dist/, runs TypeScript (for declarations), and bundles the source with esbuild. npm exec install-donobu-plugin infers the plugin name from package.json and copies dist/ into the Donobu plugins directory. Restart Donobu after installing so the new bundle is loaded.
esbuild considerations
The generated esbuild.config.mjs bundles your TypeScript source and all third-party dependencies into a single dist/index.mjs, while keeping donobu external (it is injected at runtime by the plugin loader). The mainFields: ['module', 'main'] option ensures esbuild prefers ESM entry points from third-party packages for cleaner output.
If you add dependencies that include CJS modules using require() for Node builtins (common in Google Cloud, AWS, or gRPC libraries), the bundle may fail at runtime with "Dynamic require of X is not supported". Fix this by adding a banner that provides a real require():
await build({
// ... your existing options ...
banner: {
js: 'import { createRequire } from "module"; const require = createRequire(import.meta.url);',
},
});This injects a real require() via Node's createRequire, allowing bundled CJS code to resolve Node builtins normally in the ESM context.
Recommended development loop
- Make changes in
src/ npm run build && npm exec install-donobu-plugin- Restart Donobu Studio or the backend process
- Trigger your tool from the UI or API to verify behavior
Troubleshooting
- Plugin not appearing: Ensure
npm exec install-donobu-pluginran successfully and thatdist/index.mjsexists. Restart Donobu and watch the logs for plugin loading messages. - "Cannot find package 'donobu'": Your bundle contains a runtime
import ... from "donobu". Ensure alldonobuimports useimport typeand access runtime values throughdeps.donobu. - "Dynamic require of X is not supported": A bundled CJS dependency calls
require()for a Node builtin. Add thecreateRequirebanner to your esbuild config (see above). - Schema errors: Zod throws runtime errors when inputs don't match your schema. Log the error message to quickly see which field failed.
