Agent-zero a0-create-plugin

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.

install
source · Clone the upstream repo
git clone https://github.com/agent0ai/agent-zero
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/agent0ai/agent-zero "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/a0-create-plugin" ~/.claude/skills/agent0ai-agent-zero-a0-create-plugin && rm -rf "$T"
manifest: skills/a0-create-plugin/SKILL.md
source content

Agent Zero Plugin Development

[!IMPORTANT] Always create new plugins in

/a0/usr/plugins/<plugin_name>/
. The
/a0/plugins/
directory is reserved for core system plugins.

Related skills:

/a0/skills/a0-review-plugin/SKILL.md
|
/a0/skills/a0-contribute-plugin/SKILL.md
|
/a0/skills/a0-manage-plugin/SKILL.md

Primary references:

  • /a0/AGENTS.md (Full-stack architecture & AgentContext)
  • /a0/docs/agents/AGENTS.components.md (Component system deep dive)
  • /a0/docs/agents/AGENTS.modals.md (Modal system & CSS conventions)
  • /a0/docs/agents/AGENTS.plugins.md (Extension points, plugin.yaml, settings system, Plugin Index)
  • /a0/docs/developer/plugins.md (Developer lifecycle and publishing)

Step 0: Ask First — Local or Community Plugin?

Before starting, ask the user one question:

"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)?"

  • Local plugin: Create it in
    /a0/usr/plugins/<plugin_name>/
    . No repository needed. Skip to the manifest section below.
  • 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.

Plugin Manifest (plugin.yaml)

Every plugin must have a

plugin.yaml
or it will not be discovered.

name: my_plugin              # required for community plugins; must match dir name (^[a-z0-9_]+$)
title: My Plugin
description: What this plugin does.
version: 1.0.0
settings_sections:
  - agent
per_project_config: false
per_agent_config: false

name
: lowercase, numbers, underscores only (
^[a-z0-9_]+$
). Required by CI when submitting to the Plugin Index - must exactly match the index folder name.

settings_sections
controls which Settings tabs show a subsection for this plugin. Valid values:
agent
,
external
,
mcp
,
developer
,
backup
. Use
[]
for no subsection.

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).


Mandatory Frontend Patterns

1. The "Store Gate" Template

To avoid race conditions and undefined errors, every component must use this wrapper:

<div x-data>
  <template x-if="$store.myPluginStore">
    <div x-init="$store.myPluginStore.onOpen()" x-destroy="$store.myPluginStore.cleanup()">
       <!-- Content goes here -->
    </div>
  </template>
</div>

2. Separate Store Module

Place store logic in a separate .js file. Do NOT use alpine:init listeners inside HTML.

// webui/my-store.js
import { createStore } from "/js/AlpineStore.js";
export const store = createStore("myPluginStore", {
    status: 'idle',
    init() { ... },
    onOpen() { ... },
    cleanup() { ... }
});

Import it in the HTML <head>:

<head>
  <script type="module" src="/plugins/<plugin_name>/webui/my-store.js"></script>
</head>

3. User Feedback: A0 Notifications Only

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.

  • Errors:
    toastFrontendError(message, "My Plugin")
    (or
    $store.notificationStore.frontendError(...)
    )
  • Success:
    toastFrontendSuccess(message, "My Plugin")
  • Warnings/Info:
    toastFrontendWarning
    ,
    toastFrontendInfo
    from
    /components/notifications/notification-store.js

Import and call from your store; do not render a dedicated error/success block in the template. See Notifications for the full API.


Plugin Settings

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
).

Settings modal contract

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:

<html>
<head>
  <title>My Plugin Settings</title>
  <script type="module">
    import { store } from "/components/plugins/plugin-settings-store.js";
  </script>
</head>
<body>
  <div x-data>
    <input x-model="config.my_key" />
    <input type="checkbox" x-model="config.feature_enabled" />
  </div>
</body>
</html>

The modal's Save button persists

config
to
config.json
in the correct scope (project/agent/global).

Sidebar Button (sidebar entry point)

  • Extension point:
    sidebar-quick-actions-main-start
  • Class:
    class="config-button"
  • Placement:
    x-move-after=".config-button#dashboard"
  • Action:
    @click="openModal('/plugins/<plugin_name>/webui/my-modal.html')"

Backend API & Context

Import Paths

  • Correct:
    from agent import AgentContext, AgentContextType
  • Correct:
    from initialize import initialize_agent
  • Correct for plugin-local Python modules under
    usr/plugins/<name>/
    :
    from usr.plugins.<name>.helpers.module import ...
  • Avoid
    sys.path
    hacks for plugin-local imports
  • Avoid symlink-dependent imports like
    from plugins.<name>...
    for user/community plugins in
    usr/plugins/

Sending Messages Proactively

from agent import AgentContext
from helpers.messages import UserMessage

context = AgentContext.use(context_id)
task = context.communicate(UserMessage("Message text"))
response = await task.result()

Reading Plugin Settings (backend)

from helpers.plugins import get_plugin_config, save_plugin_config

# Runtime (with running agent - resolves project/profile from context)
settings = get_plugin_config("my-plugin", agent=agent) or {}

# Explicit write target (project/profile scope)
save_plugin_config(
    "my-plugin",
    project_name="my-project",
    agent_profile="default",
    settings=settings,
)

Directory Layout

/a0/usr/plugins/<name>/
  plugin.yaml           # Required manifest
  execute.py            # Optional user-triggered setup, post-install, or maintenance script
  hooks.py              # Optional framework runtime hook functions
  default_config.yaml   # Optional default settings fallback
  README.md             # Optional locally; strongly recommended for community plugins
  LICENSE               # Optional locally (shown in Plugin List UI when present); required at repo root for Plugin Index submission
  agents/
    <profile>/agent.yaml # Optional plugin-distributed agent profile
  api/                  # API Handlers (ApiHandler base class)
  tools/                # Tool subclasses
  helpers/              # Shared Python logic
  prompts/              # Prompt templates
  conf/
    model_providers.yaml # Optional: add or override model providers
  extensions/
    python/<extension_point>/  # Named Python lifecycle extensions
    python/_functions/<module>/<qualname>/<start|end>/  # Implicit @extensible hooks
    webui/<point>/      # HTML/JS hook extensions
  webui/
    config.html         # Optional: plugin settings UI
    my-modal.html       # Full plugin pages
    my-store.js         # Alpine stores

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.

Import rule for plugin-local Python code

Use the fully qualified

usr.plugins.<plugin_name>...
path for plugin-local imports. This lets plugins keep a normal
helpers/
directory without renaming it to
<name>_helpers
, and it avoids both
sys.path
mutation and symlink installation steps.

Good:

from usr.plugins.my_plugin.helpers.runtime import do_work
import usr.plugins.my_plugin.helpers.state as state

Avoid:

sys.path.insert(0, ...)
from helpers.runtime import do_work

from plugins.my_plugin.helpers.runtime import do_work

Plugin Execution Script (
execute.py
)

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.

Good uses for

execute.py
include:

  • installing dependencies or downloading models/assets
  • running post-install steps after the plugin is copied into place
  • rebuilding caches, indexes, or generated files
  • applying migrations, repair steps, or sync jobs that the user may need to run again later
  • performing periodic maintenance tasks that should happen only when explicitly requested by the user

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.

First rule of plugin side effects: do not modify the system permanently in ways that outlive the plugin. When a plugin is deleted, there should be no leftover symlinks, unmanaged services, or stray files outside plugin-owned paths unless the user explicitly requested that behavior and the plugin documents how to clean it up.

import subprocess
import sys

def main():
    print("Installing plugin dependencies...")
    result = subprocess.run(
        [sys.executable, "-m", "pip", "install", "requests==2.31.0"],
        text=True,
    )
    if result.returncode != 0:
        print("ERROR: Installation failed")
        return result.returncode

    print("Refreshing plugin resources...")
    # Add post-install, repair, migration, or maintenance logic here.

    print("Done.")
    return 0

if __name__ == "__main__":
    sys.exit(main())

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.

Runtime Hooks (
hooks.py
)

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(...)
.

  • hooks.py
    runs inside the Agent Zero framework runtime, not the separate agent execution environment.
  • Use it for things like install hooks, pre-update hooks, plugin registration work, cache setup, file preparation, or other internal framework operations.
  • Current built-in usage:
    • the plugin installer calls
      install()
      in
      hooks.py
      after placing a plugin in
      usr/plugins/
    • the plugin updater calls
      pre_update()
      in
      hooks.py
      immediately before pulling new plugin code into place
    • 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()
  • Hook functions may be sync or async.
  • Hooks should be reversible and cleanup-safe. Prefer framework-managed state and plugin-owned paths over permanent system modifications.

Environment targeting rules

  • If
    hooks.py
    runs
    sys.executable -m pip install ...
    , it installs into the same Python environment that is running Agent Zero.
  • That is correct for dependencies needed by the plugin inside the framework runtime.
  • If the dependency is meant for the separate agent runtime or for OS-level tools, do not assume the current environment is correct.

Instead, explicitly switch targets in a subprocess:

  • invoke the exact Python interpreter for the target runtime
  • activate the target virtualenv in the subprocess before running
    pip
  • run the relevant OS package manager from a subprocess configured for the intended environment

In Docker, this usually means

hooks.py
affects
/opt/venv-a0
unless you intentionally target
/opt/venv
or another environment.


Community Plugin: GitHub Repo + Plugin Index Submission

If the user chose a community plugin, follow these additional steps after building and testing the plugin locally.

1. Repository Structure

The plugin must live in its own GitHub repository with the plugin contents at the repository root (not inside a subfolder):

your-plugin-repo/          ← GitHub repository root
├── plugin.yaml            ← runtime manifest (must include name field!)
├── default_config.yaml
├── README.md
├── LICENSE                ← required at repo root before Plugin Index submission
├── api/
├── tools/
├── extensions/
└── webui/

The runtime

plugin.yaml
at the repo root must include a
name
field
matching the index folder name:

name: my_plugin            # REQUIRED - must match index folder name exactly
title: My Plugin
description: What this plugin does.
version: 1.0.0

Help the user create this repository and push the plugin files to it.

2. Index manifest (different from runtime manifest)

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:

title: My Plugin
description: What this plugin does.
github: https://github.com/yourname/your-plugin-repo
tags:
  - tools
  - example
screenshots:                # optional, up to 5 full image URLs
  - https://raw.githubusercontent.com/yourname/your-plugin-repo/main/docs/screen1.png

Required fields:

title
,
description
,
github
. Optional:
tags
(up to 5),
screenshots
(up to 5 URLs). See the recommended tag list at https://github.com/agent0ai/a0-plugins/blob/main/TAGS.md.

Important: CI also checks that your remote

plugin.yaml
contains a
name
field matching the index folder name exactly.

3. Submission steps

  1. Fork
    https://github.com/agent0ai/a0-plugins
    .
  2. Create the folder
    plugins/<your_plugin_name>/
    in the fork.
    • Folder name: lowercase letters, numbers, underscores only (
      ^[a-z0-9_]+$
      ) - no hyphens
    • Must exactly match the
      name
      field in your remote
      plugin.yaml
  3. Add
    index.yaml
    inside it (and optionally a square thumbnail ≤ 20 KB named
    thumbnail.png
    ,
    thumbnail.jpg
    , or
    thumbnail.webp
    ).
  4. Open a Pull Request. The PR must add exactly one new plugin folder.
  5. CI validates automatically. A maintainer reviews and merges.

Submission constraints:

  • Folder name: unique, stable,
    ^[a-z0-9_]+$
  • Folders starting with
    _
    are reserved for internal use
  • title
    max 50 characters,
    description
    max 500 characters
  • index.yaml
    max 2000 characters total

For a fully guided contribution flow (including git operations), read

/a0/skills/a0-contribute-plugin/SKILL.md
.


Plugin Index & Plugin Hub

The Plugin Index is the community hub at https://github.com/agent0ai/a0-plugins.

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.