| 1 | --- |
| 2 | name: a0-create-plugin |
| 3 | description: Create, extend, or modify Agent Zero plugins. Follows strict full-stack conventions (usr/plugins, plugin.yaml, Store Gating, AgentContext, plugin settings). Use for UI hooks, API handlers, lifecycle extensions, or plugin settings UI. |
| 4 | version: 1.0.0 |
| 5 | tags: ["plugins", "create", "build", "develop", "extend"] |
| 6 | trigger_patterns: |
| 7 | - "create plugin" |
| 8 | - "build plugin" |
| 9 | - "new plugin" |
| 10 | - "develop plugin" |
| 11 | - "write plugin" |
| 12 | - "plugin template" |
| 13 | --- |
| 14 | |
| 15 | # Agent Zero Plugin Development |
| 16 | |
| 17 | > [!IMPORTANT] |
| 18 | > Always create new plugins in `/a0/usr/plugins/<plugin_name>/`. The `/a0/plugins/` directory is reserved for core system plugins. |
| 19 | |
| 20 | Related skills: `/a0/skills/a0-review-plugin/SKILL.md` | `/a0/skills/a0-contribute-plugin/SKILL.md` | `/a0/skills/a0-manage-plugin/SKILL.md` |
| 21 | |
| 22 | Primary references: |
| 23 | - /a0/AGENTS.md (Full-stack architecture & AgentContext) |
| 24 | - /a0/plugins/AGENTS.md (Plugin contract, plugin.yaml, settings, banners, extension contracts, Plugin Index) |
| 25 | - /a0/webui/components/AGENTS.md (Component system and modal component conventions) |
| 26 | - /a0/webui/js/AGENTS.md (Modal stack, API helpers, extension loader) |
| 27 | - /a0/webui/css/AGENTS.md (Modal CSS and shared visual primitives) |
| 28 | - /a0/docs/developer/plugins.md (Developer lifecycle and publishing) |
| 29 | |
| 30 | --- |
| 31 | |
| 32 | ## Step 0: Ask First — Local or Community Plugin? |
| 33 | |
| 34 | Before starting, ask the user one question: |
| 35 | |
| 36 | > "Should this plugin be **local only** (stays in your Agent Zero installation) or a **community plugin** (published to the Plugin Index so others can install it)?" |
| 37 | |
| 38 | - **Local plugin**: Create it in `/a0/usr/plugins/<plugin_name>/`. No repository needed. Skip to the manifest section below. |
| 39 | - **Community plugin**: The plugin must live in its own GitHub repository (runtime manifest at the repo root), and then a separate index submission PR is made to https://github.com/agent0ai/a0-plugins. Guide the user through both steps. |
| 40 | |
| 41 | --- |
| 42 | |
| 43 | ## Plugin Manifest (plugin.yaml) |
| 44 | |
| 45 | Every plugin must have a `plugin.yaml` or it will not be discovered. |
| 46 | |
| 47 | ```yaml |
| 48 | name: my_plugin # required for community plugins; must match dir name (^[a-z0-9_]+$) |
| 49 | title: My Plugin |
| 50 | description: What this plugin does. |
| 51 | version: 1.0.0 |
| 52 | settings_sections: |
| 53 | - agent |
| 54 | per_project_config: false |
| 55 | per_agent_config: false |
| 56 | ``` |
| 57 | |
| 58 | `name`: lowercase, numbers, underscores only (`^[a-z0-9_]+$`). Required by CI when submitting to the Plugin Index - must exactly match the index folder name. |
| 59 | |
| 60 | `settings_sections` controls which Settings tabs show a subsection for this plugin. Valid values: `agent`, `external`, `mcp`, `developer`, `backup`. Use `[]` for no subsection. |
| 61 | |
| 62 | Activation defaults to ON when no toggle rule exists. Set `per_project_config` and/or `per_agent_config` to enable advanced per-scope switching. Core system plugins may also use `always_enabled: true` to lock the plugin permanently ON (reserved for framework use). |
| 63 | |
| 64 | --- |
| 65 | |
| 66 | ## Mandatory Frontend Patterns |
| 67 | |
| 68 | ### 1. The "Store Gate" Template |
| 69 | To avoid race conditions and undefined errors, every component must use this wrapper: |
| 70 | ```html |
| 71 | <div x-data> |
| 72 | <template x-if="$store.myPluginStore"> |
| 73 | <div x-init="$store.myPluginStore.onOpen()" x-destroy="$store.myPluginStore.cleanup()"> |
| 74 | <!-- Content goes here --> |
| 75 | </div> |
| 76 | </template> |
| 77 | </div> |
| 78 | ``` |
| 79 | |
| 80 | ### 2. Separate Store Module |
| 81 | Place store logic in a separate .js file. Do NOT use alpine:init listeners inside HTML. |
| 82 | ```javascript |
| 83 | // webui/my-store.js |
| 84 | import { createStore } from "/js/AlpineStore.js"; |
| 85 | export const store = createStore("myPluginStore", { |
| 86 | status: 'idle', |
| 87 | init() { ... }, |
| 88 | onOpen() { ... }, |
| 89 | cleanup() { ... } |
| 90 | }); |
| 91 | ``` |
| 92 | Import it in the HTML <head>: |
| 93 | ```html |
| 94 | <head> |
| 95 | <script type="module" src="/plugins/<plugin_name>/webui/my-store.js"></script> |
| 96 | </head> |
| 97 | ``` |
| 98 | |
| 99 | ### 3. User Feedback: A0 Notifications Only |
| 100 | Do **not** show errors or success via inline boxes (e.g. a red `<div>` bound to `store.error`). Use the project notification system so toasts and history stay consistent. |
| 101 | |
| 102 | - **Errors**: `toastFrontendError(message, "My Plugin")` (or `$store.notificationStore.frontendError(...)`) |
| 103 | - **Success**: `toastFrontendSuccess(message, "My Plugin")` |
| 104 | - **Warnings/Info**: `toastFrontendWarning`, `toastFrontendInfo` from `/components/notifications/notification-store.js` |
| 105 | |
| 106 | Import and call from your store; do not render a dedicated error/success block in the template. See [Notifications](/a0/docs/developer/notifications.md) for the full API. |
| 107 | |
| 108 | --- |
| 109 | |
| 110 | ## Plugin Settings |
| 111 | |
| 112 | If your plugin needs user-configurable settings, add `webui/config.html`. The system detects it automatically and shows a Settings button in the relevant tabs (per `settings_sections` in `plugin.yaml`). |
| 113 | |
| 114 | ### Settings modal contract |
| 115 | |
| 116 | The modal provides Project + Agent profile context selectors. The plugin settings wrapper instantiates a local modal context from `$store.pluginSettingsPrototype`. Inside `config.html`, bind plugin fields to `config.*` and use `context.*` for modal-level state and actions: |
| 117 | |
| 118 | ```html |
| 119 | <html> |
| 120 | <head> |
| 121 | <title>My Plugin Settings</title> |
| 122 | <script type="module"> |
| 123 | import { store } from "/components/plugins/plugin-settings-store.js"; |
| 124 | </script> |
| 125 | </head> |
| 126 | <body> |
| 127 | <div x-data> |
| 128 | <input x-model="config.my_key" /> |
| 129 | <input type="checkbox" x-model="config.feature_enabled" /> |
| 130 | </div> |
| 131 | </body> |
| 132 | </html> |
| 133 | ``` |
| 134 | |
| 135 | The modal's Save button persists `config` to `config.json` in the correct scope (project/agent/global). |
| 136 | |
| 137 | ### Sidebar Button (sidebar entry point) |
| 138 | - Extension point: `sidebar-quick-actions-main-start` |
| 139 | - Class: `class="config-button"` |
| 140 | - Placement: `x-move-after=".config-button#dashboard"` |
| 141 | - Action: `@click="openModal('/plugins/<plugin_name>/webui/my-modal.html')"` |
| 142 | |
| 143 | --- |
| 144 | |
| 145 | ## Backend API & Context |
| 146 | |
| 147 | ### Import Paths |
| 148 | - Correct: `from agent import AgentContext, AgentContextType` |
| 149 | - Correct: `from initialize import initialize_agent` |
| 150 | - Correct for plugin-local Python modules under `usr/plugins/<name>/`: `from usr.plugins.<name>.helpers.module import ...` |
| 151 | - Avoid `sys.path` hacks for plugin-local imports |
| 152 | - Avoid symlink-dependent imports like `from plugins.<name>...` for user/community plugins in `usr/plugins/` |
| 153 | |
| 154 | ### Sending Messages Proactively |
| 155 | ```python |
| 156 | from agent import AgentContext |
| 157 | from helpers.messages import UserMessage |
| 158 | |
| 159 | context = AgentContext.use(context_id) |
| 160 | task = context.communicate(UserMessage("Message text")) |
| 161 | response = await task.result() |
| 162 | ``` |
| 163 | |
| 164 | ### Reading Plugin Settings (backend) |
| 165 | ```python |
| 166 | from helpers.plugins import get_plugin_config, save_plugin_config |
| 167 | |
| 168 | # Runtime (with running agent - resolves project/profile from context) |
| 169 | settings = get_plugin_config("my-plugin", agent=agent) or {} |
| 170 | |
| 171 | # Explicit write target (project/profile scope) |
| 172 | save_plugin_config( |
| 173 | "my-plugin", |
| 174 | project_name="my-project", |
| 175 | agent_profile="default", |
| 176 | settings=settings, |
| 177 | ) |
| 178 | ``` |
| 179 | |
| 180 | ### Configuration Hook Caller Context |
| 181 | |
| 182 | Use caller context only when the same settings need different behavior for a |
| 183 | known origin. An unlabeled call remains compatible and uses `"api"`; a |
| 184 | plugin-controlled runtime path can opt in explicitly: |
| 185 | |
| 186 | ```python |
| 187 | settings = get_plugin_config("my-plugin", agent=agent, caller="agent") or {} |
| 188 | ``` |
| 189 | |
| 190 | Its `hooks.py` receives `hook_context={"caller": ...}`. For example, a plugin |
| 191 | can redact a stored credential for a UI-specific path while preserving its |
| 192 | normal runtime configuration: |
| 193 | |
| 194 | ```python |
| 195 | def get_plugin_config(default=None, hook_context=None, **kwargs): |
| 196 | caller = (hook_context or {}).get("caller", "api") |
| 197 | return redact_for_display(default) if caller == "ui" else default |
| 198 | ``` |
| 199 | |
| 200 | The available values are `"ui"`, `"agent"`, and `"api"`. Existing hooks do |
| 201 | not need to change: the framework safely ignores this new argument for hooks |
| 202 | that do not accept it. This is behavior metadata, never authorization; do not |
| 203 | use it to grant or deny access to secrets or other protected data. A |
| 204 | `config.html` alone does not set the caller; its backend load/save path must |
| 205 | pass it explicitly. |
| 206 | |
| 207 | --- |
| 208 | |
| 209 | ## Directory Layout |
| 210 | ``` |
| 211 | /a0/usr/plugins/<name>/ |
| 212 | plugin.yaml # Required manifest |
| 213 | execute.py # Optional user-triggered setup, post-install, or maintenance script |
| 214 | hooks.py # Optional framework runtime hook functions |
| 215 | default_config.yaml # Optional default settings fallback |
| 216 | README.md # Optional locally; strongly recommended for community plugins |
| 217 | LICENSE # Optional locally (shown in Plugin List UI when present); required at repo root for Plugin Index submission |
| 218 | agents/ |
| 219 | <profile>/agent.yaml # Optional plugin-distributed agent profile |
| 220 | api/ # API Handlers (ApiHandler base class) |
| 221 | tools/ # Tool subclasses |
| 222 | helpers/ # Shared Python logic |
| 223 | prompts/ # Prompt templates |
| 224 | conf/ |
| 225 | model_providers.yaml # Optional: add or override model providers |
| 226 | extensions/ |
| 227 | python/<extension_point>/ # Named Python lifecycle extensions |
| 228 | python/_functions/<module>/<qualname>/<start|end>/ # Implicit @extensible hooks |
| 229 | webui/<point>/ # HTML/JS hook extensions |
| 230 | webui/ |
| 231 | config.html # Optional: plugin settings UI |
| 232 | my-modal.html # Full plugin pages |
| 233 | my-store.js # Alpine stores |
| 234 | ``` |
| 235 | |
| 236 | Do not create the retired flattened extensible path form `extensions/python/<module>_<qualname>_<start|end>/`. The current runtime only resolves the deep `_functions/<module>/<qualname>/<start|end>` layout for implicit `@extensible` hooks. |
| 237 | |
| 238 | ### Import rule for plugin-local Python code |
| 239 | |
| 240 | Use the fully qualified `usr.plugins.<plugin_name>...` path for plugin-local |
| 241 | imports. This lets plugins keep a normal `helpers/` directory without renaming |
| 242 | it to `<name>_helpers`, and it avoids both `sys.path` mutation and symlink |
| 243 | installation steps. |
| 244 | |
| 245 | Good: |
| 246 | |
| 247 | ```python |
| 248 | from usr.plugins.my_plugin.helpers.runtime import do_work |
| 249 | import usr.plugins.my_plugin.helpers.state as state |
| 250 | ``` |
| 251 | |
| 252 | Avoid: |
| 253 | |
| 254 | ```python |
| 255 | sys.path.insert(0, ...) |
| 256 | from helpers.runtime import do_work |
| 257 | |
| 258 | from plugins.my_plugin.helpers.runtime import do_work |
| 259 | ``` |
| 260 | |
| 261 | ## Plugin Execution Script (`execute.py`) |
| 262 | If your plugin needs a user-triggered script for setup, post-install work, maintenance, or other manual operations, add an `execute.py` at the plugin root. |
| 263 | |
| 264 | Good uses for `execute.py` include: |
| 265 | - installing dependencies or downloading models/assets |
| 266 | - running post-install steps after the plugin is copied into place |
| 267 | - rebuilding caches, indexes, or generated files |
| 268 | - applying migrations, repair steps, or sync jobs that the user may need to run again later |
| 269 | - performing periodic maintenance tasks that should happen only when explicitly requested by the user |
| 270 | |
| 271 | Use `execute.py` for **user-initiated** work. If the behavior is framework-internal or should happen automatically as part of plugin lifecycle handling, use `hooks.py` or lifecycle extensions instead. |
| 272 | |
| 273 | First rule of plugin side effects: do not modify the system permanently in ways |
| 274 | that outlive the plugin. When a plugin is deleted, there should be no leftover |
| 275 | symlinks, unmanaged services, or stray files outside plugin-owned paths unless |
| 276 | the user explicitly requested that behavior and the plugin documents how to |
| 277 | clean it up. |
| 278 | |
| 279 | ```python |
| 280 | import subprocess |
| 281 | import sys |
| 282 | |
| 283 | def main(): |
| 284 | print("Installing plugin dependencies...") |
| 285 | result = subprocess.run( |
| 286 | [sys.executable, "-m", "pip", "install", "requests==2.31.0"], |
| 287 | text=True, |
| 288 | ) |
| 289 | if result.returncode != 0: |
| 290 | print("ERROR: Installation failed") |
| 291 | return result.returncode |
| 292 | |
| 293 | print("Refreshing plugin resources...") |
| 294 | # Add post-install, repair, migration, or maintenance logic here. |
| 295 | |
| 296 | print("Done.") |
| 297 | return 0 |
| 298 | |
| 299 | if __name__ == "__main__": |
| 300 | sys.exit(main()) |
| 301 | ``` |
| 302 | |
| 303 | Users trigger it from the Plugins UI. Treat it as a manual, rerunnable operation: return `0` on success, non-zero on failure, and print progress so the user can understand what happened. When possible, make it safe to run more than once; if reruns are not safe, detect the state and print a clear message. |
| 304 | |
| 305 | ## Runtime Hooks (`hooks.py`) |
| 306 | If your plugin needs framework-internal hook points, add a `hooks.py` file at the plugin root. The framework can call exported functions by name via `helpers.plugins.call_plugin_hook(...)`. |
| 307 | |
| 308 | - `hooks.py` runs inside the **Agent Zero framework runtime**, not the separate agent execution environment. |
| 309 | - Use it for things like install hooks, pre-update hooks, plugin registration work, cache setup, file preparation, or other internal framework operations. |
| 310 | - Current built-in usage: |
| 311 | - the plugin installer calls `install()` in `hooks.py` after placing a plugin in `usr/plugins/` |
| 312 | - the plugin updater calls `pre_update()` in `hooks.py` immediately before pulling new plugin code into place |
| 313 | - the plugin uninstaller calls `uninstall()` in `hooks.py` before deleting the plugin directory — use this to clean up any dependencies or state created by `install()` |
| 314 | - Hook functions may be sync or async. |
| 315 | - Hooks should be reversible and cleanup-safe. Prefer framework-managed state and plugin-owned paths over permanent system modifications. |
| 316 | |
| 317 | ### Environment targeting rules |
| 318 | - If `hooks.py` runs `sys.executable -m pip install ...`, it installs into the same Python environment that is running Agent Zero. |
| 319 | - That is correct for dependencies needed by the plugin inside the framework runtime. |
| 320 | - If the dependency is meant for the separate agent runtime or for OS-level tools, do **not** assume the current environment is correct. |
| 321 | |
| 322 | Instead, explicitly switch targets in a subprocess: |
| 323 | - invoke the exact Python interpreter for the target runtime |
| 324 | - activate the target virtualenv in the subprocess before running `pip` |
| 325 | - run the relevant OS package manager from a subprocess configured for the intended environment |
| 326 | |
| 327 | In Docker, this usually means `hooks.py` affects `/opt/venv-a0` unless you intentionally target `/opt/venv` or another environment. |
| 328 | |
| 329 | --- |
| 330 | |
| 331 | ## Community Plugin: GitHub Repo + Plugin Index Submission |
| 332 | |
| 333 | If the user chose a **community plugin**, follow these additional steps after building and testing the plugin locally. |
| 334 | |
| 335 | ### 1. Repository Structure |
| 336 | |
| 337 | The plugin must live in its own GitHub repository with the plugin contents at the **repository root** (not inside a subfolder): |
| 338 | |
| 339 | ```text |
| 340 | your-plugin-repo/ ← GitHub repository root |
| 341 | ├── plugin.yaml ← runtime manifest (must include name field!) |
| 342 | ├── default_config.yaml |
| 343 | ├── README.md |
| 344 | ├── LICENSE ← required at repo root before Plugin Index submission |
| 345 | ├── api/ |
| 346 | ├── tools/ |
| 347 | ├── extensions/ |
| 348 | └── webui/ |
| 349 | ``` |
| 350 | |
| 351 | The runtime `plugin.yaml` at the repo root **must include a `name` field** matching the index folder name: |
| 352 | |
| 353 | ```yaml |
| 354 | name: my_plugin # REQUIRED - must match index folder name exactly |
| 355 | title: My Plugin |
| 356 | description: What this plugin does. |
| 357 | version: 1.0.0 |
| 358 | ``` |
| 359 | |
| 360 | Help the user create this repository and push the plugin files to it. |
| 361 | |
| 362 | ### 2. Index manifest (different from runtime manifest) |
| 363 | |
| 364 | The Plugin Index (`https://github.com/agent0ai/a0-plugins`) uses a **separate `index.yaml`** file that only describes discoverability — it is NOT the same as the runtime `plugin.yaml` and has a different schema: |
| 365 | |
| 366 | ```yaml |
| 367 | title: My Plugin |
| 368 | description: What this plugin does. |
| 369 | github: https://github.com/yourname/your-plugin-repo |
| 370 | tags: |
| 371 | - tools |
| 372 | - example |
| 373 | screenshots: # optional, up to 5 full image URLs |
| 374 | - https://raw.githubusercontent.com/yourname/your-plugin-repo/main/docs/screen1.png |
| 375 | ``` |
| 376 | |
| 377 | Required fields: `title`, `description`, `github`. Optional: `tags` (up to 5), `screenshots` (up to 5 URLs). |
| 378 | See the recommended tag list at https://github.com/agent0ai/a0-plugins/blob/main/TAGS.md. |
| 379 | |
| 380 | > Important: CI also checks that your remote `plugin.yaml` contains a `name` field matching the index folder name exactly. |
| 381 | |
| 382 | ### 3. Submission steps |
| 383 | |
| 384 | 1. Fork `https://github.com/agent0ai/a0-plugins`. |
| 385 | 2. Create the folder `plugins/<your_plugin_name>/` in the fork. |
| 386 | - Folder name: lowercase letters, numbers, underscores only (`^[a-z0-9_]+$`) - no hyphens |
| 387 | - Must exactly match the `name` field in your remote `plugin.yaml` |
| 388 | 3. Add `index.yaml` inside it (and optionally a square thumbnail ≤ 20 KB named `thumbnail.png`, `thumbnail.jpg`, or `thumbnail.webp`). |
| 389 | 4. Open a Pull Request. The PR must add exactly one new plugin folder. |
| 390 | 5. CI validates automatically. A maintainer reviews and merges. |
| 391 | |
| 392 | Submission constraints: |
| 393 | - Folder name: unique, stable, `^[a-z0-9_]+$` |
| 394 | - Folders starting with `_` are reserved for internal use |
| 395 | - `title` max 50 characters, `description` max 500 characters |
| 396 | - `index.yaml` max 2000 characters total |
| 397 | |
| 398 | For a fully guided contribution flow (including git operations), read `/a0/skills/a0-contribute-plugin/SKILL.md`. |
| 399 | |
| 400 | --- |
| 401 | |
| 402 | ## Plugin Index & Plugin Hub |
| 403 | |
| 404 | The **Plugin Index** is the community hub at https://github.com/agent0ai/a0-plugins. |
| 405 | |
| 406 | Agent Zero now exposes indexed plugins through the built-in **Plugin Hub**. Users can open it from the **Plugins** dialog either through the **Browse** tab or through the **Install** button, then inspect plugin details and install directly from the UI. |