skills/a0-create-plugin/SKILL.md
[!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:
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)?"
/a0/usr/plugins/<plugin_name>/. No repository needed. Skip to the manifest section below.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).
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>
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>
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.
toastFrontendError(message, "My Plugin") (or $store.notificationStore.frontendError(...))toastFrontendSuccess(message, "My Plugin")toastFrontendWarning, toastFrontendInfo from /components/notifications/notification-store.jsImport and call from your store; do not render a dedicated error/success block in the template. See Notifications for the full API.
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).
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-quick-actions-main-startclass="config-button"x-move-after=".config-button#dashboard"@click="openModal('/plugins/<plugin_name>/webui/my-modal.html')"from agent import AgentContext, AgentContextTypefrom initialize import initialize_agentusr/plugins/<name>/: from usr.plugins.<name>.helpers.module import ...sys.path hacks for plugin-local importsfrom plugins.<name>... for user/community plugins in usr/plugins/from agent import AgentContext
from helpers.messages import UserMessage
context = AgentContext.use(context_id)
task = context.communicate(UserMessage("Message text"))
response = await task.result()
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,
)
/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.
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
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:
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.
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.install() in hooks.py after placing a plugin in usr/plugins/pre_update() in hooks.py immediately before pulling new plugin code into placeuninstall() in hooks.py before deleting the plugin directory — use this to clean up any dependencies or state created by install()hooks.py runs sys.executable -m pip install ..., it installs into the same Python environment that is running Agent Zero.Instead, explicitly switch targets in a subprocess:
pipIn Docker, this usually means hooks.py affects /opt/venv-a0 unless you intentionally target /opt/venv or another environment.
If the user chose a community plugin, follow these additional steps after building and testing the plugin locally.
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.
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.yamlcontains anamefield matching the index folder name exactly.
https://github.com/agent0ai/a0-plugins.plugins/<your_plugin_name>/ in the fork.
^[a-z0-9_]+$) - no hyphensname field in your remote plugin.yamlindex.yaml inside it (and optionally a square thumbnail ≤ 20 KB named thumbnail.png, thumbnail.jpg, or thumbnail.webp).Submission constraints:
^[a-z0-9_]+$_ are reserved for internal usetitle max 50 characters, description max 500 charactersindex.yaml max 2000 characters totalFor a fully guided contribution flow (including git operations), read /a0/skills/a0-contribute-plugin/SKILL.md.
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.