Extensibility
Plugins
Frontend-registered extensions for components, commands, views, assistant tools, executors, and lifecycle hooks.
What plugins can add
React components
Custom UI panels and embedded components.
Commands
Explicit UI actions and slash-command style behaviors.
YAML views
Plugin-owned view documents persisted under the plugin views path.
Assistant tools
Function-style tools that the built-in assistant can call at runtime.
Executors
Runtime handlers for plugin tools and plugin-owned actions.
Lifecycle hooks
Startup and teardown logic via onLoad and onUnload.
Plugin models
In-tree plugins
Live under frontend/src/plugins, are imported during app startup, and are built with the main frontend. This is the model used by built-in plugin-style extensions such as coding agents.
Runtime-installed npm plugins
Installed with zorai install plugin <npm-package-or-local-path>. They ship as self-contained browser scripts that register themselves through window.ZoraiApi.registerPlugin(...).
Runtime interface
export interface Plugin {
id: string;
name: string;
version: string;
components?: Record<string, React.ComponentType<any>>;
commands?: Record<string, CommandAction>;
views?: Record<string, unknown>;
assistantTools?: PluginAssistantToolDefinition[];
assistantToolExecutors?: Record<string, PluginAssistantToolExecutor>;
onLoad?: () => void;
onUnload?: () => void;
}
Names are namespaced automatically. Components become ${plugin.id}:${name}. The same namespacing logic applies to commands.
Packaging contract
Runtime-installed plugins should expose a zoraiPlugin field in package.json.
{
"name": "zorai-plugin-example",
"version": "0.6.3",
"zoraiPlugin": {
"entry": "dist/zorai-plugin.js",
"format": "script"
}
}
entryis required.- Only
format: "script"is currently supported. - The entry bundle must be self-contained and directly executable in the renderer.
- Published packages must already contain built entry assets because installers run with
npm install --ignore-scripts.
Views, commands, and tools
| Extension point | When to use it | Typical output |
|---|---|---|
| Components | You need custom UI | Custom panels and renderable nodes |
| Commands | You need explicit user-triggered actions | Buttons, command palette actions, slash-style actions |
| Views | You own a whole surface | Plugin YAML views under views/plugins |
| Assistant tools | The built-in assistant should call your plugin directly | Function-tool style schemas + runtime execution |
A plugin should not add a core view just because it can. Use a plugin when the feature is truly plugin-owned or vertical-specific.
AutoResearch plugin example
The repository includes an AutoResearch plugin that demonstrates a complete runtime-installed integration:
- plugin metadata (
name,version, compatibility) - settings fields for configuration
- assistant tools for research workflows
- executors that handle plugin-specific actions
- lifecycle hooks for setup and teardown
Reference the AutoResearch plugin in frontend/src/plugins/autoresearch for a working example of how to structure commands, tools, and executors.
Author workflow
- Decide whether the plugin is in-tree or runtime-installed.
- Define the plugin
id, surface area, and namespaced commands/components. - Create views and components only where the plugin truly owns a workflow.
- Add assistant tools only when the built-in assistant should invoke the plugin directly.
- Package as a self-contained script if you want runtime installation.
- Install with
zorai install pluginand validate in the actual UI runtime.
Detailed plugin authoring guide
There are two practical authoring paths in the current plugin system:
API-backed plugin
Use this when the plugin is primarily a wrapper over a remote HTTP API. Gmail and Google Calendar are the reference examples in this repo.
Python-executable plugin
Use this when the plugin drives a local workflow, repository tool, or executable. AutoResearch is the reference example in this repo.
Pattern 1: API plugin (Gmail / Calendar example)
The Gmail and Calendar plugins show the shape of an API-first Zorai plugin. At the package level, the published npm package simply ships plugin directories plus a README:
zorai-plugin-gmail-calendar/
├── package.json
├── README.md
├── gmail/
│ ├── plugin.json
│ └── skills/
│ └── gmail-inbox.md
└── calendar/
├── plugin.json
└── skills/
└── calendar-today.md
That package contains two separate plugins, one for Gmail and one for Calendar. Each plugin gets its own plugin.json, commands, settings, auth block, API endpoints, and skills.
Step 1: package your plugin for distribution
The outer package.json is simple. It mainly declares the npm package metadata and which plugin directories are published:
{
"name": "zorai-plugin-gmail-calendar",
"version": "1.0.0",
"files": [
"gmail/",
"calendar/",
"README.md"
]
}
If you are publishing a single plugin, your files array would usually contain just one plugin directory plus optional docs.
Step 2: create plugin.json
The real plugin contract lives in the plugin directory. For Gmail, that starts like this:
{
"name": "gmail",
"version": "1.1.0",
"schema_version": 1,
"description": "Gmail integration — read, search, send, trash, and manage emails",
"zorai_version": ">=2.0.0"
}
At minimum, give the plugin a stable name, schema version, description, and Zorai compatibility range.
Step 3: define authentication
Gmail and Calendar both use OAuth2 with PKCE. The manifest-level auth block is what tells Zorai how to obtain and refresh tokens:
{
"auth": {
"type": "oauth2",
"authorization_url": "https://accounts.google.com/o/oauth2/v2/auth",
"token_url": "https://oauth2.googleapis.com/token",
"scopes": [
"https://www.googleapis.com/auth/gmail.modify",
"https://www.googleapis.com/auth/gmail.send"
],
"pkce": true
}
}
Use the auth block when the plugin needs Zorai to manage API credentials interactively instead of relying on a separate external login step.
Step 4: expose settings to the operator
The Gmail and Calendar plugins declare user-editable settings for OAuth credentials and runtime defaults:
{
"settings": {
"client_id": {
"type": "string",
"label": "Google Client ID",
"required": true,
"secret": false,
"description": "OAuth2 Client ID from Google Cloud Console"
},
"client_secret": {
"type": "string",
"label": "Google Client Secret",
"required": true,
"secret": true,
"description": "OAuth2 Client Secret from Google Cloud Console"
}
}
}
Use plugin settings for the values the operator must supply. Mark them secret: true when they should be masked in UI surfaces.
Step 5: define the API surface
This is the heart of an API plugin. You declare a base URL and a set of named endpoints. Gmail uses endpoints such as list_inbox, search_messages, send_message, and get_thread. Calendar uses endpoints such as list_events, create_event, and delete_event.
{
"api": {
"base_url": "https://www.googleapis.com",
"endpoints": {
"list_events": {
"method": "GET",
"path": "/calendar/v3/calendars/{{default params.calendar_id \"primary\"}}/events?...",
"headers": { "Authorization": "Bearer ***REDACTED***" },
"response_template": "## Calendar Events\n\n{{#each items}}...{{/each}}"
}
},
"rate_limit": {
"requests_per_minute": 60
}
}
}
Important pieces:
base_urlselects the remote service host.endpointsgives each API operation a stable internal name.pathcan template parameters into the final request URL.headersdeclares auth and content-type requirements.bodyis used for POST/PUT-style endpoints.response_templateshapes what the agent sees after the call.
Step 6: map slash commands to actions
The command layer is intentionally thin. API-backed commands point to endpoint names using action:
{
"commands": {
"inbox": {
"description": "Show recent inbox messages",
"action": "list_inbox"
},
"search": {
"description": "Search emails by query",
"action": "search_messages"
},
"today": {
"description": "Show today's calendar events",
"action": "list_events"
}
}
}
If your plugin is API-backed, this is the main pattern: command descriptions for the operator, endpoint names for the runtime.
Step 7: add plugin skills
The Gmail and Calendar examples both ship skill files and reference them directly from the manifest:
{
"skills": ["skills/gmail-inbox.md"]
}
Those skill files teach the assistant how to turn natural-language requests into the right plugin commands and parameters.
Step 8: install and validate
zorai plugin add zorai-plugin-gmail-calendar
After installation, validate three things:
- settings fields show up correctly,
- the OAuth flow succeeds,
- commands and natural-language requests route to the expected endpoint.
When to use the API pattern
Choose this pattern when your plugin is mainly a structured wrapper over a remote service and the runtime can express the integration as auth + settings + endpoints + actions + skills.
Pattern 2: Python executable plugin (AutoResearch example)
The AutoResearch plugin shows the local-executable pattern. Instead of OAuth and HTTP endpoints, it drives a workspace-local Python workflow.
zorai-plugin-autosearch/
├── package.json
├── README.md
└── autosearch/
├── plugin.json
├── scripts/
│ └── autosearch_helper.py
└── skills/
└── autosearch-train.md
This plugin is ideal when Zorai should orchestrate a local toolchain instead of a remote API.
Step 1: package the plugin
The outer npm package again just ships the plugin directory:
{
"name": "zorai-plugin-autosearch",
"version": "1.0.0",
"files": [
"autosearch/",
"README.md"
]
}
Step 2: define a minimal manifest
The AutoResearch manifest starts with the same core metadata but does not need an auth or api section:
{
"name": "autosearch",
"version": "1.0.0",
"schema_version": 1,
"description": "AutoResearch workflow integration — validate workspace, prepare data, and run training",
"zorai_version": ">=2.0.0"
}
Step 3: expose runtime settings
For local executable plugins, settings usually describe the target workspace and runtime defaults rather than remote credentials. AutoResearch uses:
{
"settings": {
"workspace_path": {
"type": "string",
"label": "Default workspace path",
"required": false,
"description": "Default local AutoResearch workspace path"
},
"default_command_timeout_sec": {
"type": "number",
"label": "Default command timeout (seconds)",
"required": false,
"default": 1800
}
}
}
Step 4: define Python defaults
The key difference is the top-level python block. AutoResearch uses:
{
"python": {
"env": true
}
}
This tells Zorai the plugin expects a Python environment bootstrap. The plugin docs also support richer inherited defaults such as:
run_pathfor workspace-relative execution,sourcefor bootstrap/download location,envfor activation behavior,dependenciesfor packages the plugin needs.
Step 5: map commands to Python execution
Instead of action, executable plugins declare a python.command string per command:
{
"commands": {
"check": {
"description": "Validate an AutoResearch workspace path",
"python": {
"command": "python scripts/autosearch_helper.py check \"${AUTOSEARCH_WORKSPACE:?set AUTOSEARCH_WORKSPACE}\""
}
},
"prepare": {
"description": "Run uv run prepare.py in an AutoResearch workspace",
"python": {
"command": "python scripts/autosearch_helper.py prepare \"${AUTOSEARCH_WORKSPACE:?set AUTOSEARCH_WORKSPACE}\""
}
},
"train": {
"description": "Run uv run train.py in an AutoResearch workspace",
"python": {
"command": "python scripts/autosearch_helper.py train \"${AUTOSEARCH_WORKSPACE:?set AUTOSEARCH_WORKSPACE}\""
}
}
}
}
This is the core executable-plugin pattern: each slash command expands to a concrete Python command line that the agent/runtime can execute.
Step 6: provide helper scripts
AutoResearch uses a helper script instead of embedding all logic in the manifest. That is the right move when validation, environment checks, workspace inspection, or multi-step execution would become messy in inline command strings.
Use helper scripts when you need to:
- validate repository/workspace shape,
- normalize environment variables,
- provide cleaner failure messages,
- wrap a more complex workflow behind simple slash commands.
Step 7: add a plugin skill
Just like the API pattern, AutoResearch ships a skill file and references it from the manifest:
{
"skills": ["skills/autosearch-train.md"]
}
The skill is what teaches the assistant when to run /autosearch.check, /autosearch.prepare, and /autosearch.train rather than guessing blindly.
Step 8: install and validate
zorai plugin add zorai-plugin-autosearch
Then validate:
- the workspace path setting is visible,
- the environment/bootstrap logic works,
- the helper script can validate the target workspace,
- prepare/train commands emit useful status and errors.
When to use the Python pattern
Choose this pattern when the plugin should orchestrate a local repository workflow, Python toolchain, or executable rather than wrap a remote API.
Choosing between API and Python plugins
| Need | Prefer | Why |
|---|---|---|
| OAuth-backed HTTP service | API plugin | Manifest can express auth, endpoints, and actions directly |
| Remote REST/JSON operations | API plugin | Response templates let Zorai shape results cleanly |
| Local repository workflow | Python plugin | Commands can wrap local tools and helper scripts |
| Workspace validation and shell execution | Python plugin | Executable commands fit better than HTTP endpoints |
| Complex local orchestration | Python plugin | Helper scripts keep the manifest simple |