Plugin Overview
TeamWeb AI’s plugin system supports five types of plugins. Each type extends the platform in a different way:
| Type | Purpose | Implementation file | Guide |
|---|---|---|---|
| Tool | Adds tools that assistants can use during conversations | tools.py | Tool Plugins |
| Channel | Adds communication channels (email, messaging, voice) | channel.py | Channel Plugins |
| LLM | Adds a language model provider | provider.py | Provider Plugins |
| Embedding | Adds an embedding model provider | provider.py | Provider Plugins |
| Output | Adds structured output type definitions for tasks | outputs.py | Output Plugins |
Plugin Structure
Every plugin lives in its own directory and must contain a manifest.py file that describes the plugin. The directory name is used as the plugin identifier during loading.
my_plugin/
__init__.py # Optional
manifest.py # Required — plugin metadata
tools.py # Required for tool plugins
channel.py # Required for channel plugins
provider.py # Required for LLM and embedding plugins
outputs.py # Required for output pluginsInstalling User Plugins
TeamWeb AI discovers plugins from two locations at startup:
- System plugins — shipped with TeamWeb AI in
app/plugins/ - User plugins — loaded from the directory set by the
PLUGIN_DIRenvironment variable (defaults to/data/plugins)
To install a custom plugin, place its directory inside the PLUGIN_DIR path:
/data/plugins/
my_tool_plugin/
manifest.py
tools.py
my_channel_plugin/
manifest.py
channel.pyRestart the application after adding or modifying plugins. TeamWeb AI logs each plugin it discovers at startup.
Technical Details
Discovery process — At application startup, the plugin registry scans both directories for subdirectories containing a manifest.py file. Each manifest is imported, validated, and registered. The registry enforces globally unique plugin names and tool names — duplicate names raise an error at startup to prevent conflicts.
Graceful degradation — If a plugin fails to load (due to an import error, missing dependency, or invalid manifest), the error is logged and other plugins continue loading normally. One broken plugin does not prevent the application from starting. Check the application logs if your plugin doesn’t appear in the UI.
Configuration storage layers — Plugin configuration is stored at different levels reflecting how each plugin type is used. Tool plugin configuration is stored per project (different projects may need different API keys). Channel plugin configuration is stored per assistant (each assistant has its own bot token or webhook settings). LLM and embedding provider configuration is stored globally (one active provider for the entire installation). Tool plugins can additionally define per-assistant tool configuration for settings that vary between assistants using the same tool.
_) are ignored during plugin discovery.The Manifest
Every plugin requires a manifest.py file that exports a PluginManifest instance named manifest:
from app.plugins.base import PluginManifest, PluginType
manifest = PluginManifest(
name="my_plugin",
plugin_type=PluginType.TOOL,
version="1.0.0",
description="A short description shown in the UI",
author="Your Name",
config_schema={
"api_key": {
"type": "string",
"required": True,
"secret": True,
"label": "API Key",
"placeholder": "sk-...",
"help_text": "Your service API key",
},
"base_url": {
"type": "string",
"required": False,
"label": "Base URL",
"default": "https://api.example.com",
},
},
sandboxable=False,
setup_docs="<p>Static HTML setup instructions.</p>",
)Manifest Fields
| Field | Type | Description |
|---|---|---|
| name | str | Unique identifier for the plugin |
| plugin_type | PluginType | One of TOOL, CHANNEL, EMBEDDING, LLM, OUTPUT |
| version | str | Semantic version string |
| description | str | Human-readable description shown in the UI |
| author | str | Plugin author name |
| config_schema | dict | Configuration fields (see below) |
| sandboxable | bool | Whether tool execution can run in a Docker sandbox (tool plugins only) |
| setup_docs | str | Static HTML with setup instructions shown in the configuration UI |
| channel_type | str | For channel plugins: "email", "messaging", or "voice". Only one channel of each type may be active per assistant |
Config Schema Fields
Each key in config_schema defines a configuration field that appears in the plugin settings form:
| Property | Type | Description |
|---|---|---|
| type | str | JSON Schema type ("string", "integer", "boolean") |
| required | bool | Whether the field must be filled in |
| secret | bool | Mask the value in the UI after saving (for API keys, tokens, etc.) |
| label | str | Display label for the form field |
| placeholder | str | Placeholder text |
| default | any | Default value |
| help_text | str | Help text shown below the field |
Dynamic Setup Docs
Instead of (or in addition to) static setup_docs, you can define a get_setup_docs function in your manifest module to generate setup instructions dynamically. This is useful for including environment-specific values like webhook URLs:
from typing import Any
def get_setup_docs(app_config: dict[str, Any], **kwargs: Any) -> str:
"""Return setup instructions with the actual webhook URL."""
public_url = app_config.get("PUBLIC_URL", "https://yourdomain.com")
webhook_url = f"{public_url}/webhooks/channels/my_channel/inbound"
return (
"<h6>Setup</h6>"
"<ol>"
f"<li>Set your webhook URL to: <code>{webhook_url}</code></li>"
"</ol>"
)Configuration Layers
Plugin configuration is stored at different levels depending on the plugin type:
| Plugin type | Configuration level | Where it’s stored |
|---|---|---|
| Tool | Per project | ProjectPlugin.config_json |
| Channel | Per assistant | AssistantPlugin.config_json |
| LLM | Global | CorePluginConfig.config_json |
| Embedding | Global | CorePluginConfig.config_json |
| Output | None | No configuration needed |
Tool plugins also support an optional per-assistant tool configuration via the config_schema class attribute on individual BaseTool subclasses. This is separate from the plugin-level config and is stored in AssistantTool.config_json. Use this when individual assistants need different settings for the same tool (e.g., a default WordPress category).
Accessing the Plugin Registry
The plugin registry is available in any Flask request or Celery task context:
from flask import current_app
registry = current_app.extensions["plugin_registry"]
# List all plugins
all_plugins = registry.get_all_plugins()
# Get plugins by type
tool_plugins = registry.get_plugins_by_type(PluginType.TOOL)
# Get a specific plugin's manifest
manifest = registry.get_plugin("my_plugin")Tips
- Unique names: Plugin names and tool names must be globally unique. The registry raises an error on duplicates.
- Error handling: If a plugin fails to load (bad import, missing dependency), TeamWeb AI logs the error and continues loading other plugins. Check the application logs if your plugin doesn’t appear.
- Dependencies: User plugins can import from
app.plugins.basesince the parent directory is added tosys.pathduring loading. For third-party packages, ensure they are installed in the application environment. - Logging: Use Python’s
loggingmodule. Logs from plugins appear in the normal application log output. - API call tracking: Always use
ctx.log_api_call()in tool plugins for any external HTTP requests. This makes debugging much easier.