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"
  }
}
  • entry is 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 pointWhen to use itTypical output
ComponentsYou need custom UICustom panels and renderable nodes
CommandsYou need explicit user-triggered actionsButtons, command palette actions, slash-style actions
ViewsYou own a whole surfacePlugin YAML views under views/plugins
Assistant toolsThe built-in assistant should call your plugin directlyFunction-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

  1. Decide whether the plugin is in-tree or runtime-installed.
  2. Define the plugin id, surface area, and namespaced commands/components.
  3. Create views and components only where the plugin truly owns a workflow.
  4. Add assistant tools only when the built-in assistant should invoke the plugin directly.
  5. Package as a self-contained script if you want runtime installation.
  6. Install with zorai install plugin and 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_url selects the remote service host.
  • endpoints gives each API operation a stable internal name.
  • path can template parameters into the final request URL.
  • headers declares auth and content-type requirements.
  • body is used for POST/PUT-style endpoints.
  • response_template shapes 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_path for workspace-relative execution,
  • source for bootstrap/download location,
  • env for activation behavior,
  • dependencies for 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

NeedPreferWhy
OAuth-backed HTTP serviceAPI pluginManifest can express auth, endpoints, and actions directly
Remote REST/JSON operationsAPI pluginResponse templates let Zorai shape results cleanly
Local repository workflowPython pluginCommands can wrap local tools and helper scripts
Workspace validation and shell executionPython pluginExecutable commands fit better than HTTP endpoints
Complex local orchestrationPython pluginHelper scripts keep the manifest simple

Plugin vs. core

Use a plugin when the feature is a domain-specific extension, owns its own UI/workflow, or adds commands/tools that should stay decoupled from Zorai core.
Use core when the feature belongs to Zorai itself: daemon state, built-in mission control, native task/goals flows, or platform-wide primitives.