Trigger Plugin Sync#
Trigger plugins (Telegram, Lark/Feishu, Gmail, and others) allow external services to start a Dify workflow via webhook or event subscription. Each trigger plugin node in a workflow graph must have a corresponding WorkflowPluginTrigger database record that maps the node to a subscription ID β production event dispatch looks up this record to determine which workflow to run. The process of creating, updating, and deleting these records is called trigger-plugin relationship sync.
Key Files#
| Layer | File |
|---|---|
| Event handler | api/events/event_handlers/sync_plugin_trigger_when_app_created.py |
| Core sync logic | api/services/trigger/trigger_service.py (lines 153β303) |
| App signals | api/events/app_event.py |
| DB models | api/models/trigger.py |
| Lark trigger plugin | triggers/lark_trigger/provider/lark.py |
How Sync Works#
The event handler subscribes to two blinker signals:
app_draft_workflow_was_syncedβ fired when a draft is saved/syncedapp_published_workflow_was_updatedβ fired when a workflow is published
On either signal, it calls TriggerService.sync_plugin_trigger_relationships(app, workflow). That method:
- Walks the workflow graph to collect all nodes of type
TRIGGER_PLUGIN_NODE_TYPE, extractingplugin_id,provider_id,event_name, andsubscription_idfrom each node config . - Enforces a hard limit of 5 Integration Trigger nodes per workflow .
- Checks a Redis cache (
plugin_trigger_nodes:{app_id}:{node_id}) before hitting the database, falling back to a DB query for cache misses . - Diffs the graph nodes against the
WorkflowPluginTriggerDB records, then creates, updates, or deletes records as needed . A Redis lock prevents concurrent creation races .
The handler prioritizes published_workflow over synced_draft_workflow when both are present, because production dispatch relies on these relationships while debug/test-run dispatch does not .
The Publish-Sync Bug (and Fix)#
Symptom: A trigger works in Test Run but sends 0 executions after publishing. Reported for Telegram (#41752), Gmail (issue #31732), and by extension any trigger plugin.
Root cause (pre-fix): The event handler was only connected to app_draft_workflow_was_synced. When a workflow was published, no sync ran against the published workflow version, so the DB records still pointed at draft-time state. Production dispatch, which resolves the workflow to run via these records, found no valid match and silently dropped the event.
Fix: PR #41756 (merged 2026-09-04) added @app_published_workflow_was_updated.connect to the handler and updated the handle() signature to accept both synced_draft_workflow and published_workflow. The selection logic (published_workflow if published_workflow is not None else synced_draft_workflow) ensures the published snapshot is reconciled at publish time .
Lark Trigger Stability Fix#
The Lark trigger plugin had a separate set of bugs fixed in PR #2078 (merged 2025-11-17):
- Removed strict config validation:
_capture()no longer raisesValueErrorwhenlark_encrypt_keyis absent β the field is optional . _on_eventsignature update: All event handler methods now accept an additionalpayload: Mapping[str, Any]argument to match the SDK interface .- Import path corrections: Several event modules were importing from
examples.*instead of the local package. - Plugin name and version: Renamed to
lark_trigger; version bumped to0.0.2.
The fixed LarkTrigger._dispatch_event builds a lark.EventDispatcherHandler, dispatches the incoming webhook request, and collects event type strings (e.g., "message_receive_v1") via a thread-local cache before returning an EventDispatch response.
Constraints and Gotchas#
- Only
AppMode.WORKFLOWis supported. Chatflow apps are explicitly skipped by the handler . - 5-node limit. Exceeding 5 trigger plugin nodes in one workflow raises a
ValueErrorand aborts sync . - No sync without a
subscription_id. Nodes missingsubscription_idare silently skipped during the walk . - Test Run vs. Production: Test Run (debug dispatch) bypasses the
WorkflowPluginTriggerlookup; production dispatch does not β this is the fundamental reason the bug was silent in Test Run.