Skip to content

Plugin system

Upload, discovery, attachment, and runtime loading of ZIP plugins — org-scoped and system-scoped, settings schema, and the full API reference.

Intended audience: Stakeholders, Business analysts, Solution architects, Developers, Testers

Learning outcomes by role

Stakeholders

  • Summarize org versus system plugin scope and operational risk of third-party ZIP content.

Business analysts

  • Capture upload, validation, and attachment workflows for requirements.

Solution architects

  • Place S3, disk cache, and API surfaces in a secure deployment topology.

Developers

  • Implement uploads, metadata, and runtime loading consistent with PluginService APIs.

Testers

  • Exercise invalid ZIPs, schema failures, and org isolation for plugin operations.

A plugin is a ZIP archive containing a Python BasePlugin subclass and tools. Plugins move through three states before they reach users: uploaded (stored and discovered), attached (referenced in an orchestrator’s active_plugin_ids), and loaded (the running pool instance has initialized the plugin’s code path). All three must be true for chat requests to reach plugin-defined tools.

flowchart TD
    UP[POST /upload — multipart ZIP] --> VAL[read_validated_plugin_file<br>magic bytes PK\x03\x04 + .zip extension + ≤50 MB]
    VAL --> SVC[plugin_service.upload_*_plugin]
    SVC --> DISC[Discover BasePlugin subclass<br>in plugin.py]
    DISC --> DB[(Store row: pid, version, name,<br>description, capabilities, settings_schema)]
    DB --> CATALOG[Org catalog / system catalog]

    ORCH[Orchestrator active_plugin_ids reference] --> POOL[Pool load → SDKPluginBundle]
    POOL --> BUNDLE[contract + agent + uv_tools<br>+ bound_model + orchestrator_tools]
    BUNDLE --> GRAPH[Injected into agent graph nodes]
    GRAPH --> CHAT[Chat request → tools available]

Upload stores the ZIP and immediately runs discovery to find the BasePlugin subclass — no separate registration step is required.

Attachment means an orchestrator row’s active_plugin_ids array includes the plugin id. Saving the orchestrator config does not affect the running pool instance.

Loading means the pool has initialized the plugin for a running orchestrator instance. If the orchestrator was running before the upload or attachment, a load or reload is required. See Hot-reload and orchestrator pool.

Both system and org plugins share these core fields:

FieldTypeNotes
idUUIDDatabase primary key
pidstringReverse-domain identifier (e.g. com.example.search) — globally unique registry key
versionstringSemantic version (MAJOR.MINOR or MAJOR.MINOR.PATCH)
namestringHuman-readable display name
descriptionstringPlugin capability description
tagstring | nullOptional category tag for filtering
capabilitiesstring[]Capability tag list (e.g. ["search", "retrieval"])
is_specializedbooleanAgent is a BaseSpecializedAgent (multi-agent topology)
is_scopedbooleanAgent is a BaseScopedAgent (grounded mode support)
statelessbooleanPlugin declares stateless operation
settings_schemaJSON[]Per-key settings definition for UI and initialization
default_settingsJSONDefault values for settings keys
is_latestbooleanWhether this is the most recent version for its pid
enabledbooleanWhether the plugin is available for use
logo_imagestring | nullBase64 or URL logo for the UI

Each entry in settings_schema has this shape:

FieldTypeRequiredNotes
keystringyesMachine-readable setting identifier
namestringnoDisplay label (defaults to key if omitted)
typestringyesOne of str, int, float, bool, list, dict
descriptionstringyesHuman-readable description shown in UI
defaultanynoDefault value; type must match type
requiredbooleannoIf true, must be provided before the plugin can load
sensitivebooleannoMasked in logs and UI; stored encrypted
MethodPathPermissionDescription
GET/api/orgs/{org_id}/pluginscadence:org:plugins:readList org + system plugins combined; filter with ?tag=
GET/api/orgs/{org_id}/plugins/{pid}/versionscadence:org:plugins:readAll versions of a plugin by pid; ?source=org|system
POST/api/orgs/{org_id}/plugins/uploadcadence:org:plugins:writeUpload a ZIP plugin to the org catalog
PATCH/api/orgs/{org_id}/plugins/{plugin_id}cadence:org:plugins:writeEnable or disable an org plugin
DELETE/api/orgs/{org_id}/plugins/{plugin_id}cadence:org:plugins:writeDisable an org plugin (sets enabled=false)
GET/api/orgs/{org_id}/plugins/{pid}/settings-schemacadence:org:plugins:readFetch settings schema for dynamic UI rendering
MethodPathPermissionDescription
GET/api/admin/pluginscadence:system:plugins:readList all system plugins; ?include_disabled=true (default)
POST/api/admin/plugins/uploadcadence:system:plugins:writeUpload a ZIP to the system catalog
GET/api/admin/plugins/{pid}/versionscadence:system:plugins:readList all versions for a system plugin
GET/api/admin/plugins/{plugin_id}cadence:system:plugins:readGet one system plugin by ID
PATCH/api/admin/plugins/{plugin_id}cadence:system:plugins:writeEnable or disable a system plugin
DELETE/api/admin/plugins/{plugin_id}cadence:system:plugins:writeDisable a system plugin
  1. The client posts multipart/form-data with a file field to the upload endpoint.
  2. read_validated_plugin_file validates the file: extension must be .zip, first 4 bytes must be PK\x03\x04 (ZIP magic bytes), and size must not exceed 50 MB.
  3. plugin_service.upload_*_plugin extracts the ZIP to the filesystem and runs the discovery process — it imports plugin.py from the archive and scans for a BasePlugin subclass.
  4. The discovered class’s get_metadata() and get_settings_schema() are called to extract pid, version, name, description, capabilities, and the settings schema.
  5. A row is written to the plugin catalog. The response returns id, pid, version, and org_id.
cadence/api/plugin.py
zip_bytes = await read_validated_plugin_file(file)
plugin_service = request.app.state.plugin_service
plugin = await plugin_service.upload_organization_plugin(
org_id=context.org_id,
zip_bytes=zip_bytes,
caller_id=context.user_id,
org_domain=org_domain,
)
return {
"message": "Plugin uploaded successfully",
"id": str(plugin.id),
"pid": plugin.pid,
"version": plugin.version,
"org_id": context.org_id,
}

Org plugins are uploaded by org admins and appear only in that org’s catalog. Reference them in active_plugin_ids on any orchestrator in that org.

System plugins are uploaded by sys_admin and are globally available. They appear in every org’s plugin list alongside that org’s private plugins. Use system plugins for platform capabilities that all tenants should be able to enable.

How it works — settings schema and initialization

Section titled “How it works — settings schema and initialization”

When an org admin configures a plugin, the UI calls GET /api/orgs/{org_id}/plugins/{pid}/settings-schema to retrieve the declared settings. Filled-in values are stored in the orchestrator’s plugin_settings JSON field and passed as config to agent.initialize(config) when the orchestrator loads.

cadence/api/plugin.py
schema = plugin_service.get_settings_schema(plugin_pid, context.org_id)
return [PluginSettingSchema(**setting) for setting in schema]
  1. Upload the plugin ZIP and note the returned pid.
  2. Create or update an orchestrator with the pid in active_plugin_ids (see the JSON example below for POST /api/orgs/{org_id}/orchestrators).
  3. Load the orchestrator instance into the pool. The pool will call create_agent() on the plugin class and bundle its tools into the graph.
  4. Send a chat request — tool calls now route through the plugin.
POST /api/orgs/{org_id}/orchestrators
{
"name": "Support Agent",
"framework_type": "langgraph",
"mode": "supervisor",
"active_plugin_ids": ["com.example.support-tools"],
"config": { "default_llm_config_id": "<uuid>" }
}

A valid plugin ZIP must contain at

my_plugin.zip
├── plugin.py # Required — must define a BasePlugin subclass
└── pyproject.toml # Required — package metadata

Additional Python files, data files, and sub - packages are allowed.The discovery process importsplugin.py and scans all top - level names for BasePlugin subclasses.

SymptomCauseFix
400 Invalid plugin fileWrong file extension, bad magic bytes, or file > 50 MBUse.zip file under 50 MB; re - zip if file is corrupted
Upload succeeds but no tools in chatPlugin not in active_plugin_ids, or instance not reloadedVerifyactive_plugin_ids; reload instance via pool API
404 on settings - schemaPlugin pid not in registry for this orgVerify the plugin is uploaded andenabled=true
Tools appear but fail at runtimerequired settings not filled inCheckplugin_settings on the orchestrator; fill all required fields
Version confusion after re - uploadRunning instance has stale codeReload the orchestrator viaPOST .../load
Plugin discovered butvalidate_dependencies failsPython package missing in plugin’s pyproject.tomlAdd the missing package and re-upload
System plugin not visible to orgPlugin isenabled=falseEnable viaPATCH /api/admin/plugins/{id}