Skip to content

Overview

Plugins are event-driven extensions that respond to platform events. They run automatically when triggered by events and can perform actions like sending notifications, processing data, or integrating with external services.

A plugin is a directory under plugins/ containing:

my_plugin/
├── plugin.json # Manifest: metadata, events, variables
└── handler.py # Event handler functions

That’s it. No setup.py, no backend/ subdirectory. Just two files at the root of the plugin directory.

Plugins can also extend the platform with Flask routes (web pages and API endpoints) by providing a routes.py file with a create_blueprint() function. See the Route Registration guide for details.

my_plugin/
├── plugin.json # Manifest: metadata, events, variables, routes
├── handler.py # Event handler functions
├── routes.py # Flask route handlers (optional)
└── templates/ # Jinja2 templates (optional)
└── page.html

The plugin manifest defines the plugin’s metadata and events it responds to:

{
"id": "my_plugin",
"name": "My Custom Plugin",
"version": "1.0.0",
"description": "Plugin that does something when events occur",
"events": [
"message_received",
"turn_complete"
],
"variables": [
{
"name": "CONFIG_KEY",
"type": "string",
"description": "Configuration variable description"
}
]
}
FieldTypeRequiredDescription
idstringYesUnique plugin identifier (lowercase, alphanumeric + underscores)
namestringYesDisplay name for the plugin
versionstringYesSemantic version (e.g. 1.0.0)
descriptionstringYesShort description of what the plugin does
eventsstring[]YesList of event names this plugin subscribes to
variablesobject[]NoConfiguration variables users can set

Each event in the events array maps to a handler function named on_<event_name> in handler.py. See Events for a list of all supported events. The handler signature is:

def on_<event_name>(event: dict, sdk: PluginSDK) -> None:
"""Handle <event_name> events."""
# Your logic here
  • event: a dict containing event-specific data (e.g. session_id, message, summary, etc.)
  • sdk: a PluginSDK instance providing helper methods (see Plugin SDK)
  • Return value: handlers should not return anything. The framework ignores return values.
def on_message_received(event, sdk):
"""Log every incoming message."""
session_id = event.get('session_id', '')
user_message = event.get('message', '')
sdk.log(f"Received message from session {session_id}: {user_message}")
def on_turn_complete(event, sdk):
"""Log when a conversation turn finishes."""
session_id = event.get('session_id', '')
sdk.log(f"Turn complete for session {session_id}")

Plugins can define configuration variables that users can set via CLI. Variable types:

TypeDescriptionExample
stringText value"https://hooks.example.com"
numberNumeric value10
booleanTrue/false valuetrue
arrayList of values["value1", "value2"]

Access variables in your handler via sdk.config:

def on_message_received(event, sdk):
webhook_url = sdk.config.get('WEBHOOK_URL', '')
min_count = int(sdk.config.get('MIN_COUNT', 5))
enabled = sdk.config.get('ENABLE_FEATURE', True)

Here’s a complete example plugin that logs all incoming messages and sends a notification when a turn completes:

{
"id": "message_logger",
"name": "Message Logger",
"version": "1.0.0",
"description": "Logs all incoming messages and notifies on turn completion",
"events": [
"message_received",
"turn_complete"
],
"variables": [
{
"name": "WEBHOOK_URL",
"type": "string",
"description": "Webhook URL for notifications"
},
{
"name": "MIN_COUNT",
"type": "number",
"description": "Minimum messages before logging"
}
]
}
def on_message_received(event, sdk):
"""Log every incoming message."""
session_id = event.get('session_id', '')
user_message = event.get('message', '')
sdk.log(f"Received: session={session_id}, message={user_message}")
def on_turn_complete(event, sdk):
"""Send notification when a turn completes."""
session_id = event.get('session_id', '')
webhook_url = sdk.config.get('WEBHOOK_URL', '').strip()
if not webhook_url:
sdk.log("Skipped: no WEBHOOK_URL configured", level="warn")
return
sdk.http_request(
method="POST",
url=webhook_url,
json={"session_id": session_id, "event": "turn_complete"}
)
sdk.log(f"Turn complete notification sent for session {session_id}")

Introduced in v0.6.77.

During plugin development, Evonic watches your plugin files for changes and reloads them automatically — no manual restart or evonic plugin reload needed.

Edit any file in your plugin directory (plugin.json, handler.py, routes.py, or templates) and the changes take effect immediately. The hot reload watches for:

  • File saves (write to disk)
  • New files added to the plugin directory
  • File deletions within the plugin directory
FileEffect on Reload
plugin.jsonManifest, events, and variables are re-read
handler.pyHandler functions are re-registered
routes.pyFlask routes are re-registered
templates/Jinja2 templates are reloaded on next render

In rare cases where the hot reload doesn’t detect your changes (e.g., editor saves to a temporary file and moves it), you can always fall back to the CLI:

Terminal window
evonic plugin reload my_plugin

Introduced in v0.2.0.

You can export any installed plugin as a ZIP archive directly from the Web UI. This is useful for:

  • Sharing plugins with other Evonic instances
  • Backing up your custom plugins
  • Distributing plugins to team members
  1. Go to Plugins (/plugins)
  2. Find the plugin you want to export
  3. Click the Export button (download icon)
  4. The browser downloads a .zip file containing the plugin’s directory
Terminal window
evonic plugin export <plugin_id> [--output ./my-plugin.zip]

Example:

Terminal window
evonic plugin export my_plugin --output ./backups/my_plugin_v1.zip

The ZIP archive contains the full plugin directory:

my_plugin.zip
├── plugin.json # Manifest with metadata, events, and variables
├── handler.py # Event handler functions
└── routes.py # Flask route handlers (if present)

Plugin configuration variables and user settings are not exported — only the plugin code and manifest.

To install a plugin from a ZIP file:

  1. Go to PluginsImport (/plugins/import)
  2. Upload the ZIP file
  3. The plugin is extracted to plugins/<plugin_id>/ and registered automatically

Or via CLI:

Terminal window
evonic plugin import ./backups/my_plugin_v1.zip

Introduced in v0.3.19.

Starting in v0.3.19, plugins can be packaged and distributed as .evop (Evonic Plugin) archives. This is a portable format designed specifically for sharing plugins between Evonic instances.

A .evop file is a renamed .zip archive with the same internal structure, making it recognizable as an Evonic plugin package at a glance.

Exporting a plugin as .evop follows the same process as the ZIP export, but the file is saved with the .evop extension:

Terminal window
# Export a plugin as .evop
evonic plugin export my_plugin --output ./dist/my_plugin.evop

Import works the same as importing any plugin archive:

Terminal window
# Import from .evop
evonic plugin import ./dist/my_plugin.evop

Or via the Web UI:

  1. Go to PluginsImport (/plugins/import)
  2. Select and upload the .evop file
  3. The plugin is extracted and registered

The .evop archive contains the exact same structure as the ZIP export:

my_plugin.evop
├── plugin.json # Manifest with metadata, events, and variables
├── handler.py # Event handler functions
└── routes.py # Flask route handlers (if present)

Configuration variables and user settings are not included — only the plugin code and manifest.

  • Use .evop when distributing plugins to other Evonic users — the extension makes the purpose clear
  • Use .zip for general archiving or when the recipient expects a standard ZIP format

Both formats are interchangeable — you can rename a .evop to .zip and it will work just as well.