This guide covers the Python Backend and Frontend WebUI plugin architecture. Use this as the definitive reference for building and extending Ctx AI.
Ctx AI uses a convention-over-configuration plugin model where runtime capabilities are discovered from the directory structure.
- Backend discovery (python/helpers/plugins.py): Resolves roots (usr/plugins/ first, then plugins/) and builds the effective set of plugins.
- Path resolution (python/helpers/subagents.py): Injects plugin paths into the agent's search space for prompts, tools, and configurations.
- Python extensions (python/helpers/extension.py): Executes lifecycle hooks from ctxai.extensions/python//.
- WebUI extensions (webui/js/extensions.js): Injects HTML/JS contributions into core UI breakpoints (x-extension).
Each plugin lives in usr/plugins/<plugin_name>/.
usr/plugins/<plugin_name>/
├── plugin.yaml # Required: Title, version, settings + activation metadata
├── initialize.py # Optional: one-time setup script (dependencies, models, etc.)
├── hooks.py # Optional: runtime hook functions callable by the framework
├── default_config.yaml # Optional: fallback settings defaults
├── README.md # Optional: shown in Plugin List UI
├── LICENSE # Optional: shown in Plugin List UI
├── conf/
│ └── model_providers.yaml # Optional: add or override model providers
├── api/ # API handlers (ApiHandler subclasses)
├── tools/ # Agent tools (Tool subclasses)
├── helpers/ # Shared Python logic
├── prompts/ # Prompt templates
├── agents/ # Agent profiles (agents/<profile>/agent.yaml)
├── extensions/
│ ├── python/<point>/ # Backend lifecycle hooks
│ └── webui/<point>/ # UI HTML/JS contributions
└── webui/
├── config.html # Optional: Plugin settings UI
└── ... # Full plugin pages/components
This is the manifest file that lives inside your plugin directory and drives runtime behavior. It is distinct from the index manifest used when publishing to the Plugin Index (see Section 7).
title: My Plugin
description: What this plugin does.
version: 1.0.0
settings_sections:
- agent
per_project_config: false
per_agent_config: false
# Optional: lock plugin permanently ON in UI/back-end
always_enabled: false
# Optional compatibility fields
framework_version: ">=0.1.0"
plugin_dependencies: []Field reference:
title: UI display namedescription: Short plugin summaryversion: Plugin version stringsettings_sections: Which Settings tabs show a subsection for this plugin. Valid values:agent,external,mcp,developer,backup. Use[]for no subsection.per_project_config: Enables project-scoped settings and toggle rulesper_agent_config: Enables agent-profile-scoped settings and toggle rulesalways_enabled: Forces ON and disables toggle controls in the UI (reserved for framework use)framework_version: Minimum Ctx AI version required for this pluginplugin_dependencies: Required plugin names that must be installed and enabled
Plugins can include an optional hooks.py file at the plugin root. Ctx AI loads this module on demand and calls exported functions by name through helpers.plugins.call_plugin_hook(...).
hooks.pyruns inside the Ctx AI framework runtime and Python environment, not the separate agent execution environment.- Use it for framework-internal operations such as install-time setup, plugin registration work, filesystem preparation, cache updates, or other tasks that need access to Ctx AI internals.
- Hook functions may be synchronous or async. Async hooks are awaited by the framework.
- Hook modules are cached until plugin caches are cleared, so changes may require a plugin refresh/reload cycle.
Current example: the plugin installer calls install() from hooks.py after a plugin is copied into place.
- If
hooks.pyinstalls Python packages withsys.executable -m pip, those packages are installed into the same Python environment that runs Ctx AI itself. - This is the correct place for Python dependencies that your plugin's backend code needs while running inside the framework runtime.
- It is not the right place for dependencies meant only for the separate agent execution runtime or for arbitrary system-level tooling.
If your plugin needs to install packages or binaries for the agent execution environment instead of the framework runtime, launch a subprocess that explicitly activates or targets that other environment first. In practice this means invoking the correct interpreter or shell for that environment rather than relying on the current process environment. For example:
- target a specific Python interpreter path for that runtime
- activate the desired virtualenv inside a subprocess shell command before running
pip - invoke the appropriate package manager from a subprocess prepared for that environment
In Docker deployments, this distinction is especially important:
- Framework runtime:
/opt/venv-ctx - Agent execution runtime:
/opt/venv
So a hooks.py install step affects /opt/venv-ctx unless you intentionally switch to /opt/venv (or another target) inside your subprocess.
Core UI defines insertion points like . To contribute:
- Place HTML files in extensions/webui/<extension_point>/.
- Include a root x-data scope.
- Include an x-move-* directive (e.g., x-move-to-start, x-move-after="#id").
Place *.js files in extensions/webui/<extension_point>/ and export a default async function. They are called via callJsExtensions("", context).
Core JS hooks can also expose runtime UI surfaces when static HTML breakpoints are not a fit. For example, confirm_dialog_after_render runs after the shared confirm dialog is built and receives the rendered dialog/body/footer nodes plus any caller-provided extensionContext.
Plugin UI must use the A0 notification system for errors, success, and warnings. Do not render dedicated error/success boxes (e.g. a red block bound to store.error). Use the notification store so toasts and notification history stay consistent across the app.
- Frontend (Alpine/store): Import
toastFrontendError,toastFrontendSuccess,toastFrontendWarning,toastFrontendInfofrom/components/notifications/notification-store.js, or call$store.notificationStore.frontendError(message, title)etc. - Backend (Python): Use
AgentNotification.error(...),AgentNotification.success(...)fromhelpers.notification.
See Notifications for the full API.
- Add webui/config.html to your plugin.
- The plugin settings wrapper instantiates a local modal context from $store.pluginSettingsPrototype.
- Bind plugin fields to config.* and use context.* for modal-level state and actions.
- Settings are scoped per-project and per-agent automatically.
- project/.a0proj/agents//plugins//config.json
- project/.a0proj/plugins//config.json
- usr/agents//plugins//config.json
- usr/plugins//config.json
- plugins//default_config.yaml (fallback defaults)
Plugins can add or override model providers by placing a conf/model_providers.yaml inside their plugin directory. The file follows the same format as the main conf/model_providers.yaml.
At startup (and whenever a plugin is enabled/disabled), the system:
- Loads the base
conf/model_providers.yaml. - Discovers
conf/model_providers.yamlfrom all enabled plugins. - Merges them in order — matching provider IDs are overwritten, new IDs are appended.
Example plugin provider file (usr/plugins/my_plugin/conf/model_providers.yaml):
chat:
my_custom_provider:
name: My Custom LLM
litellm_provider: openai
kwargs:
api_base: https://my-llm.example.com/v1
embedding:
my_custom_embed:
name: My Embeddings
litellm_provider: openai
kwargs:
api_base: https://my-embed.example.com/v1- Global and scoped activation are independent, with no inheritance between scopes.
- Activation flags are files:
.toggle-1(ON) and.toggle-0(OFF). - UI states are
ON,OFF, andAdvanced(shown when any project/profile-specific override exists). always_enabled: trueinplugin.yamlforces ON and disables toggle controls in the UI.- The "Switch" modal is the canonical per-scope activation surface, and "Configure Plugin" keeps scope synchronized with the settings modal.
| Route | Purpose |
|---|---|
| GET /plugins// | Serve static assets |
| POST /api/plugins// | Call plugin API |
| POST /api/plugins | Management (actions: get_config, save_config, list_configs, delete_config, toggle_plugin, get_doc) |
The Plugin Index is a community-maintained repository at https://github.com/ctxos/a0-plugins that lists plugins available to the Ctx AI community. Plugins listed there can be discovered and installed by other users.
There are two completely different plugin.yaml schemas used at different stages. They must not be confused:
Runtime manifest (inside your plugin repo/directory, drives Ctx AI behavior):
title: My Plugin
description: What this plugin does.
version: 1.0.0
settings_sections:
- agent
per_project_config: false
per_agent_config: false
always_enabled: falseIndex manifest (submitted to the a0-plugins repo under plugins/<your-plugin-name>/, drives discoverability only):
title: My Plugin
description: What this plugin does.
github: https://github.com/yourname/your-plugin-repo
tags:
- tools
- exampleThe index manifest contains only four fields (title, description, github, tags) and must not include runtime fields. The github field must point to the root of a GitHub repository that itself contains a runtime plugin.yaml at the repository root.
When creating a plugin intended for the community, the plugin should be a standalone GitHub repository where the plugin directory contents live at the repo root:
your-plugin-repo/ ← GitHub repository root
├── plugin.yaml ← runtime manifest (title, description, version, ...)
├── default_config.yaml
├── README.md
├── LICENSE
├── api/
├── tools/
├── extensions/
└── webui/
Users install it locally by cloning (or downloading) the repo contents into /ctx/usr/plugins/<plugin_name>/.
- Create a GitHub repository for your plugin with the runtime
plugin.yamlat the repo root. - Fork
https://github.com/ctxos/a0-plugins. - Create a folder
plugins/<your-plugin-name>/containing only an indexplugin.yaml(and optionally a square thumbnail image ≤ 20 KB). - Open a Pull Request with exactly one new plugin folder.
- CI validates the submission automatically. A maintainer reviews and merges.
Index submission rules:
- One plugin per PR
- Folder name must be unique, stable, lowercase, kebab-case
- Folders starting with
_are reserved for internal use githubmust point to a public repo that containsplugin.yamlat its roottitlemax 50 characters,descriptionmax 500 characterstags: optional, up to 5, use recommended tags from https://github.com/ctxos/a0-plugins/blob/main/TAGS.md
A built-in Plugin Marketplace plugin (always active) will allow users to browse the Plugin Index and install or update community plugins directly from the Ctx AI UI. This section will be updated once the marketplace plugin is released.
Refer to AGENTS.md for the main framework guide.