Plugin Author Tutorial¶
This tutorial walks through creating an Orcheo plugin from scratch. By the end you will have a working plugin that registers a custom node, and you will know how to extend that pattern to edges, agent tools, triggers, and listeners.
Prerequisites¶
- Orcheo SDK installed (
uv tool install -U orcheo-sdk) - Python 3.12+
uvfor package management
Start from the template¶
The fastest path is to clone the plugin template repository:
git clone https://github.com/AI-Colleagues/orcheo-plugin-template orcheo-plugin-acme
cd orcheo-plugin-acme
Before editing any code, rename the template package metadata so your local copy
uses your real plugin name instead of the example placeholder. Update the
distribution name, package directory, and entry-point target consistently across
the template files. A typical rename from example to acme looks like this:
Then update the remaining template placeholders:
- change the project name in
pyproject.tomltoorcheo-plugin-acme - update the
orcheo.pluginsentry point inpyproject.tomlto targetorcheo_plugin_acme:plugin - rename the plugin metadata in
src/orcheo_plugin_acme/orcheo_plugin.tomlto match the new package - replace any remaining
examplereferences in the README, tests, and module docstrings
After that rename, the file paths in the rest of this tutorial will match your plugin checkout.
1. Understand the plugin contract¶
An Orcheo plugin is a Python distribution package that:
- Declares
plugin_api_version,orcheo_version, andexportsin anorcheo_plugin.tomlmanifest bundled inside the package. - Exposes an entry point in the
orcheo.pluginsgroup pointing to an object with aregister(api)method. - Calls only the stable
PluginAPIsurface insideregister— never mutates Orcheo internals directly.
Orcheo discovers and loads plugins through the managed plugin directory at
~/.orcheo/plugins/. The CLI is the only supported install path.
2. Register a custom node¶
Edit src/orcheo_plugin_acme/__init__.py:
from __future__ import annotations
from langchain_core.runnables import RunnableConfig
from orcheo.graph.state import State
from orcheo.nodes.base import TaskNode
from orcheo.nodes.registry import NodeMetadata
from orcheo.plugins import PluginAPI
class AcmeNode(TaskNode):
"""Example node that returns a greeting."""
async def run(self, state: State, config: RunnableConfig) -> dict[str, str]:
del state, config
return {"greeting": "Hello from the Acme plugin!"}
class AcmePlugin:
def register(self, api: PluginAPI) -> None:
api.register_node(
NodeMetadata(
name="AcmeNode",
description="Returns a greeting from the Acme plugin.",
category="acme",
),
AcmeNode,
)
plugin = AcmePlugin()
The module-level plugin object is the entry-point target referenced in
pyproject.toml.
3. Install the plugin locally¶
Verify it is registered:
4. Test the plugin¶
Smoke check with doctor¶
doctor validates the managed plugin venv, manifest integrity, API
compatibility, and importability without making any changes.
Unit tests¶
Add a test that registers the plugin in isolation so you catch interface regressions before users do:
import pytest
from orcheo.plugins import PluginAPI
from orcheo_plugin_acme import AcmeNode, AcmePlugin
def test_plugin_registers_node() -> None:
api = PluginAPI()
AcmePlugin().register(api)
assert "AcmeNode" in api.registrations.nodes
@pytest.mark.asyncio()
async def test_acme_node_returns_greeting() -> None:
node = AcmeNode(name="test")
result = await node.run({}, {})
assert "greeting" in result
Run with:
The plugin template repository ships with equivalent tests you can adapt.
5. Register additional component types¶
Edge¶
from orcheo.edges.base import BaseEdge
from orcheo.edges.registry import EdgeMetadata
from orcheo.graph.state import State
from langchain_core.runnables import RunnableConfig
class AcmeEdge(BaseEdge):
async def run(self, state: State, config: RunnableConfig) -> str:
del state, config
return "default"
api.register_edge(
EdgeMetadata(
name="AcmeEdge",
description="Routes all traffic to the default branch.",
category="acme",
),
AcmeEdge,
)
Add "edges" to exports in orcheo_plugin.toml.
Agent tool¶
from langchain_core.tools import tool
from orcheo.nodes.agent_tools.registry import ToolMetadata
@tool
def acme_lookup(query: str) -> str:
"""Look up information from the Acme service."""
return f"Acme result for: {query}"
api.register_agent_tool(
ToolMetadata(
name="acme_lookup",
description="Look up information from the Acme service.",
category="acme",
),
acme_lookup,
)
Add "agent_tools" to exports in orcheo_plugin.toml.
Trigger¶
from orcheo.triggers.registry import TriggerMetadata
api.register_trigger(
TriggerMetadata(
id="acme-webhook",
display_name="Acme Webhook",
description="Fires a workflow when the Acme webhook fires.",
),
lambda **kwargs: {"config": kwargs},
)
Add "triggers" to exports in orcheo_plugin.toml.
Listener¶
Listener plugins are the most involved. See the WeCom and Lark reference plugins for complete, tested implementations:
A listener must supply:
- a
ListenerMetadatawith a stable platformid - a compiler hook (
default_listener_compilercovers most cases) - an adapter factory that creates a per-subscription adapter
import asyncio
from datetime import datetime
from orcheo.listeners.models import ListenerHealthSnapshot, ListenerSubscription
from orcheo.listeners.registry import ListenerMetadata, default_listener_compiler
class AcmeListenerAdapter:
"""Manages one long-lived connection for the Acme platform."""
def __init__(
self,
*,
repository: object,
subscription: ListenerSubscription,
runtime_id: str,
) -> None:
self._repository = repository
self._subscription = subscription
self._runtime_id = runtime_id
async def run(self, stop_event: asyncio.Event) -> None:
"""Hold the connection until the stop event is set."""
# Connect to the Acme platform, receive events, dispatch payloads.
await stop_event.wait()
def health(self) -> ListenerHealthSnapshot:
return ListenerHealthSnapshot(
subscription_id=self._subscription.id,
runtime_id=self._runtime_id,
status="healthy",
platform=self._subscription.platform,
last_polled_at=datetime.now(),
)
api.register_listener(
ListenerMetadata(
id="acme-listener",
display_name="Acme Listener",
description="Listens for events from the Acme platform.",
),
default_listener_compiler,
lambda *, repository, subscription, runtime_id: AcmeListenerAdapter(
repository=repository,
subscription=subscription,
runtime_id=runtime_id,
),
)
Add "listeners" to exports in orcheo_plugin.toml so Orcheo can
classify listener installs and updates correctly.
Note
Trigger and listener changes always require a process restart to become active. The CLI surfaces this guidance when you install or update a plugin that exports these component types.
6. Manage the plugin lifecycle¶
| Command | What it does |
|---|---|
orcheo plugin list |
Show all installed plugins and their status. |
orcheo plugin show orcheo-plugin-acme |
Show manifest, exports, and resolved install state. |
orcheo plugin install . |
Install from the local source directory. |
orcheo plugin update orcheo-plugin-acme |
Re-install from the stored source reference. |
orcheo plugin update --all |
Re-install all plugins. |
orcheo plugin disable orcheo-plugin-acme |
Prevent Orcheo from loading the plugin at startup. |
orcheo plugin enable orcheo-plugin-acme |
Re-enable a previously disabled plugin. |
orcheo plugin uninstall orcheo-plugin-acme |
Remove the plugin from the managed environment. |
orcheo plugin doctor |
Run diagnostics against all installed plugins. |
Impact classification¶
When you update or uninstall a plugin, the CLI classifies the change:
- Silent hot reload — additive node, edge, or agent-tool change. Applies to new runs without a restart.
- Confirmation-required hot reload — a component is replaced or removed. The CLI shows an impact summary and asks for confirmation.
- Restart or reconcile required — any trigger or listener change. Restart the affected processes after the operation completes.
Use --force to skip interactive confirmation in scripted environments:
7. Distribute the plugin¶
Build a wheel:
Publish to PyPI (or a private index) with standard tooling. Users install by package name, pinned version, or Git URL:
orcheo plugin install orcheo-plugin-acme
orcheo plugin install "orcheo-plugin-acme==0.1.0"
orcheo plugin install "git+https://github.com/acme-corp/orcheo-plugin-acme.git"
8. Multi-process deployments¶
In a Docker Compose or multi-worker deployment, backend and Celery workers run
in separate processes. Plugin state lives under ~/.orcheo/plugins/ (or
ORCHEO_PLUGIN_DIR). To propagate a plugin change:
- Run
orcheo plugin install,update,uninstall,enable, ordisableon each host. When using the managed stack from the host CLI, pass--runtime stackto target the backend runtime directly. Or mount a sharedORCHEO_PLUGIN_DIR. - Restart the affected backend and worker processes. Nodes, edges, and agent tools support per-process hot reload for additive changes, but triggers and listeners always require a restart.
9. Reference: validation plugins¶
Two production-grade listener plugins are available as reference implementations:
- orcheo-plugin-wecom-listener — WeCom long-connection listener
- orcheo-plugin-lark-listener — Lark listener
Read their source before building a listener plugin of your own. The Canvas
template template-wecom-lark-shared-listener shows how both listeners feed
into one shared downstream workflow — the recommended pattern when normalising
events from multiple platforms.
Related references¶
- Plugin Template — standalone starter repository
- Plugin Reference — full authoring API reference
- CLI Reference — complete
orcheo plugincommands - SDK Reference — programmatic access to plugin services
- Releasing — release process for this repository's own plugins