Webhook Trigger System#
Dify's webhook trigger system lets external HTTP clients drive workflow execution. When a Webhook Trigger node is placed at the root of a Workflow app, Dify assigns it a unique, publicly reachable URL. Incoming requests are validated, parameters extracted into typed workflow variables, and execution enqueued asynchronously via Celery β while the caller immediately receives the configured HTTP response.
Scope constraints:
- Available only for Workflow applications (not Chatbot / Chatflow)
- Maximum 5 webhook trigger nodes per workflow
Architecture#
External HTTP Request
β
βΌ
Flask Controller (/triggers/webhook/<id> | /triggers/webhook-debug/<id>)
β
βΌ
WebhookService (extract β validate β type-coerce β build inputs)
β async via Celery
βΌ
TriggerWebhookNode (ROOT node; reads variable pool β typed output vars)
| Layer | File | Role |
|---|---|---|
| Controller | api/controllers/trigger/webhook.py | Production + debug Flask routes; accepts all HTTP methods |
| Service | api/services/trigger/webhook_service.py | Core logic: extraction, validation, type coercion, async dispatch, lifecycle sync |
| Workflow node | api/core/workflow/nodes/trigger_webhook/node.py | TriggerWebhookNode β ROOT node, maps injected data to output variables |
| Entities | api/core/workflow/nodes/trigger_webhook/entities.py | WebhookData, ContentType, WebhookParameter, WebhookBodyParameter Pydantic models |
| DB model | api/models/trigger.py | WorkflowWebhookTrigger β stores app_id, node_id, tenant_id, webhook_id |
| URL builder | api/core/trigger/utils/endpoint.py | Builds production and debug URLs from dify_config.TRIGGER_URL |
Request Lifecycle#
-
Lookup & gate-check β
get_webhook_trigger_and_workflow()resolves theWorkflowWebhookTriggerDB record, verifies the linkedAppTriggerisENABLED(raisesQuotaExceededErrorifRATE_LIMITED), and fetches the publishedWorkflowand its node config. -
Content-length guard β Requests exceeding
dify_config.WEBHOOK_REQUEST_BODY_MAX_SIZEare rejected413 Payload Too Largebefore any body parsing. -
Extraction β
extract_webhook_data()routes body parsing by content-type:application/jsonβorjsonparseapplication/x-www-form-urlencodedβrequest.formdictmultipart/form-dataβ form fields + file uploads viaToolFileManagerapplication/octet-streamβ binary saved as a tool file; MIME detected viapython-magictext/plain/ unknown β raw string under keyraw
-
Validation β HTTP method and content-type are checked by
_validate_http_metadata(); required headers by_validate_required_headers(); required query/body params during_process_parameters(). -
Type coercion β Form-data string values are cast to
Number,Boolean, etc. via_convert_form_value(); JSON values are validated in-place via_validate_json_value(). -
Async dispatch β
trigger_workflow_execution()reserves a trigger quota, creates/fetches anEndUserof typeTRIGGER, and callsAsyncWorkflowService.trigger_workflow_async(). Quota is refunded on failure. -
Synchronous response β
generate_webhook_response()reads the node's configuredstatus_code(default200) andresponse_bodyand returns immediately. The workflow executes asynchronously.
Node Configuration & Parameter Extraction#
Node settings are governed by the WebhookData Pydantic model:
| Field | Default | Description |
|---|---|---|
method | GET | Accepted HTTP method |
content_type | application/json | Expected Content-Type |
headers | [] | WebhookParameter list β always extracted as String |
params | [] | WebhookParameter list β String, Number, or Boolean |
body | [] | WebhookBodyParameter list β additionally supports Object, File, and Array[*] variants |
status_code | 200 | HTTP status returned immediately to caller |
response_body | "" | Synchronous response body |
timeout | 30 | Seconds (advisory) |
Inside TriggerWebhookNode._run(), the node reads all pre-injected variables from the variable pool via get_by_prefix(self.id), then calls _extract_configured_outputs() which:
- Strips headers case-insensitively and normalizes hyphens to underscores.
- Reads query params from
webhook_data["query_params"]. - For
text/plain, wraps the raw string in the configured variable. - For
application/octet-stream, builds aFileVariable. - For structured bodies, resolves each body param by name; file-type params become
FileVariableobjects. - Always outputs
_webhook_rawβ the full rawwebhook_datadict β for debugging.
Default node config for new nodes is defined in get_default_config() with async mode enabled and a 30-second timeout.
Webhook Lifecycle Management#
ID & URL Generation#
Each webhook trigger node receives a unique 24-character URL-safe random ID from generate_webhook_id(). The WorkflowWebhookTrigger DB record has unique constraints on both (app_id, node_id) and webhook_id .
Sync on Draft Save#
When a workflow draft is saved, the app_draft_workflow_was_synced signal fires sync_webhook_when_app_created.py , which calls WebhookService.sync_webhook_relationships() with remove_stale=False. This method diffs the graph's webhook node IDs against DB records β creating rows for new nodes but preserving stale records from deleted nodes β using a Redis-backed cache (TTL 1 hour) and distributed lock to reduce DB churn. Stale records are preserved so undo operations can restore webhook trigger nodes without changing their URLs.
Sync on Publish#
When a workflow is published, the app_published_workflow_was_updated signal fires the handle_published() event handler, which calls WebhookService.sync_webhook_relationships() with remove_stale=True. At this point, stale webhook records β those corresponding to deleted nodes β are actually deleted from the database along with their Redis cache entries. This two-phase approach (preserve on draft, cleanup on publish) ensures undo operations work correctly while eventually removing obsolete webhook URLs once changes are published.
Debug vs. Production#
The debug endpoint (/webhook-debug/<id>) resolves the draft workflow, skips quota checks and Celery enqueueing, and instead fires a WebhookDebugEvent via TriggerDebugEventBus to the Variable Inspector. If no listener is registered, it returns 409 Conflict.
Frontend URL Registration#
When a TriggerWebhook node is added or changed in the workflow editor, the useAutoGenerateWebhookUrl hook calls fetchWebhookUrl() to register the URL and stores webhook_url / webhook_debug_url on the node. It skips re-generation if URLs already exist (idempotent).
Undo/Redo Behavior#
The workflow history system tracks NodeAdd / NodeDelete events and restores full graph state on undo/redo, but has no webhook-specific backend sync callbacks. When a webhook node add is undone, the frontend removes the node from the graph but the backend WorkflowWebhookTrigger record created by useAutoGenerateWebhookUrl is not deleted. Because draft saves preserve stale webhook records (remove_stale=False), redoing the node add will restore the original webhook URL. Cleanup of stale records only happens during publish operations via the handle_published() event handler.
Quota Enforcement#
Trigger quota is reserved before dispatch. On exhaustion, AppTriggerService.mark_tenant_triggers_rate_limited() sets the tenant's triggers to RATE_LIMITED, causing all subsequent production webhook requests for that tenant to return 429 Too Many Requests.