back to docs

Triggers & Webhooks

Event-driven automation in dForge: triggers run actions when records change, webhooks notify external services. Both share the same event model.

published

Two Systems, Same Events

dForge has two event-driven systems that watch the same record events but do different things when they fire:

System Config file What happens Use case
Triggers logic/triggers.json Runs a DSL action (sync or async) Internal automation: notifications, cascades, workflows
Webhooks logic/webhooks.json Sends an HTTP POST to an external URL External integration: Slack, Zapier, custom services

Both fire on the same event types: insert, update, delete, status_change, and any.

Event Types

Event Fires when Old values available
insert A new record is created No
update Any field on an existing record changes Yes
delete A record is deleted Yes (the deleted row)
status_change A specific field’s value changes (compares old vs new) Yes
any All of the above Depends

Triggers

triggers.json

{
  "triggers": [
    {
      "code": "on_opportunity_won",
      "description": "Notify owner when opportunity is closed as won",
      "entity": "opportunity",
      "event": "status_change",
      "condition": "[stage] = 'Closed Won'",
      "action": "on_won_notify",
      "async": true
    }
  ]
}
Field Required Description
code Yes Unique identifier within the module
entity Yes Entity to watch (can reference other modules’ entities)
event Yes One of insert, update, delete, status_change, any
condition No Formula evaluated against record data — trigger only fires if true
action Yes Action code to execute (must be defined in ui/actions.json)
async No true (default) queues a background job; false runs in the same transaction
params No Static parameters passed to the action
enabled No Default true. Set false to disable without removing

Conditions

Conditions use the same formula syntax as canExecute on actions:

[status] = 'Active'
[amount] > 1000
[stage] = 'Closed Won' AND [priority] = 'High'

The condition is parsed at module install time and evaluated against the record at event time. If it doesn’t match, the trigger doesn’t fire (fail-closed).

Sync vs Async

Async triggers ("async": true, the default):

  • Queue a row in the background_action table
  • A background worker picks them up within ~10 seconds
  • Run in their own transaction
  • No deadlocks, no cascades inside the originating transaction
  • Use for notifications, emails, logs, non-critical side effects

Sync triggers ("async": false):

  • Execute immediately, inside the originating transaction
  • Atomic with the original mutation — if the trigger fails, the whole change rolls back
  • Risk of infinite loops, so the platform enforces a max nesting depth of 5 and a per-record re-entrancy guard
  • Use for cascading field updates, validation, derived state

Trigger-Only Actions

If an action should fire only from triggers (never from a UI button), set its canExecute to false. Background triggers bypass canExecute, so the action still runs from the trigger but the button is hidden in the UI:

canExecute:
    false

execute:
    notify([owner_id], 'Automated: ' + [title] + ' completed')

Webhooks

webhooks.json

{
  "subscriptions": [
    {
      "code": "on_task_completed",
      "entity": "task",
      "event": "status_change",
      "condition": "[status] = 'completed'",
      "endpointUrl": "https://hooks.example.com/dforge",
      "secretCd": "webhook_signing_key",
      "payload": {
        "include": ["task_id", "title", "status"],
        "includeOld": true
      }
    }
  ]
}
Field Required Description
code Yes Unique identifier within the module
entity Yes Entity to watch
event Yes Event type
condition No Formula — only fire if true
endpointUrl No HTTP endpoint to POST to (can be set in the admin UI later)
secretCd No Reference to a secret used for HMAC-SHA256 signing
payload.include No Array of fields to include — omit for all
payload.includeOld No true to include old values for update/delete
enabled No Default true

Payload

{
  "deliveryId": "163053801702424576",
  "event": "status_change",
  "timestamp": "2026-03-26T22:34:25Z",
  "subscription": { "code": "on_task_completed", "module": "tasks" },
  "folder": { "folderId": 123 },
  "entity": "task",
  "recordId": { "task_id": 456 },
  "data": { "task_id": 456, "title": "My Task", "status": "completed" },
  "changes": { "status": { "old": "in_progress", "new": "completed" } }
}

Delivery and Retry

  • Background service polls every 5 seconds
  • Exponential backoff: immediate → 30s → 2m → 10m → 1h
  • Max 5 attempts, then marked failed
  • Headers include X-DForge-Event, X-DForge-Entity, X-DForge-Subscription, and X-DForge-Signature
  • Delivery history is retained for 30 days

HMAC Signing

When secretCd is set, the payload is signed with HMAC-SHA256 and the signature is sent in X-DForge-Signature. Verify on the receiver:

HMAC-SHA256(secret, request_body) == X-DForge-Signature

Triggers vs Webhooks: Which to Use

Scenario Use
Notify a user when a record changes Trigger → action with notify()
Send an email on status change Trigger → action with sendEmail()
Update a related record Trigger (sync) → action with field set
Post to Slack/Teams/Discord Webhook
Sync data to an external CRM/ERP Webhook
Run complex multi-step business logic Trigger → action with full DSL

Triggers stay inside dForge and have the entire DSL and database at their disposal. Webhooks are for crossing the boundary into other systems.

/ was this helpful?

Stuck on something?
Tell us.

We read every message and update the docs based on what readers ask. The fastest way to improve the docs is to write to us.