Plugin Taxonomy and Validation#
Dify plugins declare free-form tags in their manifest.yaml. Tag checkingβalong with all other manifest validationβis owned by the plugin daemon, not the Dify API layer. The API passes tags through unmodified; the daemon performs the actual filtering and any content enforcement.
Tag Field in PluginDeclaration#
Tags are modeled as an unconstrained string list in PluginDeclaration:
tags: list[str] = Field(default_factory=list)
There is no StrEnum or allow-list enforced at the Pydantic level. The API controller (PluginCategoryListQuery) accepts tags from callers as a raw list and constrains only its length (max_length=128), not individual values.
The conventional tag vocabulary used by official plugins is:
| Tag | Example plugin |
|---|---|
agent | cot_agent manifest |
rag | notion_datasource manifest |
social | discord_trigger manifest |
utilities | discord_trigger manifest |
productivity | (used by productivity-oriented official tools) |
A plugin may carry multiple tags (e.g., discord_trigger uses both social and utilities) .
Category vs. Tag#
Dify distinguishes between two classification axes:
- Category (
PluginCategory) β auto-detected from the manifest's top-level provider keys (tool,model,datasource,agent_strategy,trigger). Set by a@model_validatorinPluginDeclaration; plugins with no recognized provider key default toExtension. Category values:Tool,Model,Extension,AgentStrategy("agent-strategy"),Datasource,Trigger. - Tags β author-supplied, freeform strings for discovery/filtering. The API layer exposes a
list_by_categoryendpoint that acceptstagsas an additional filter .
Validation Architecture: Daemon-Delegated#
The Dify API never parses manifest.yaml directly. The flow is:
- On install or upload, the plugin package is forwarded to the daemon via
PluginInstaller.upload_pkg(). - The daemon parses, validates, and stores the manifest; it returns a
PluginDeclaration-shaped JSON payload. - The API deserializes the response with Pydantic
model_validate()β the only API-layer schema enforcement that occurs. - For manifest retrieval,
PluginInstaller.fetch_plugin_manifest()issues aGETtoplugin/{tenant_id}/management/fetch/manifestand deserializes the daemon's response into aPluginDeclaration. - The high-level
PluginService.fetch_plugin_manifest()wraps this call for all API consumers.
Because the API applies no tag allow-list, any tag value stored in the manifest by the daemon will round-trip through the API unchanged. Adding or removing accepted tags only requires a daemon change, not an API change.
Tag-Based Listing#
PluginService.list_by_category() accepts a tags: Sequence[str] parameter and forwards it verbatim to the daemon endpoint plugin/{tenant_id}/management/{category}/list . The daemon applies category, query, and tag filters server-side before paginating results.
Key Files#
| File | Purpose |
|---|---|
api/core/plugin/entities/plugin.py | PluginDeclaration, PluginCategory, tag field definition |
api/core/plugin/impl/plugin.py | PluginInstaller β all daemon HTTP calls including manifest fetch and tag-filtered listing |
api/core/plugin/plugin_service.py | PluginService β high-level service wrappers consumed by the console API |
dify-official-plugins/agent-strategies/cot_agent/manifest.yaml | Reference: agent tag |
dify-official-plugins/datasources/notion_datasource/manifest.yaml | Reference: rag tag |
dify-official-plugins/triggers/discord_trigger/manifest.yaml | Reference: social + utilities tags |