back to docs
[reference] triggers webhooks automation events integration

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:

SystemConfig fileWhat happensUse case
Triggerslogic/triggers.jsonRuns a DSL action (sync or async)Internal automation: notifications, cascades, workflows
Webhookslogic/webhooks.jsonSends an HTTP POST to an external URLExternal integration: Slack, Zapier, custom services

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

Event Types

EventFires whenOld values available
insertA new record is createdNo
updateAny field on an existing record changesYes
deleteA record is deletedYes (the deleted row)
status_changeA specific field’s value changes (compares old vs new)Yes
anyAll of the aboveDepends

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
    }
  ]
}
FieldRequiredDescription
codeYesUnique identifier within the module
entityYesEntity to watch (can reference other modules’ entities)
eventYesOne of insert, update, delete, status_change, any
conditionNoFormula evaluated against record data — trigger only fires if true
actionYesAction code to execute (must be defined in ui/actions.json)
asyncNotrue (default) queues a background job; false runs in the same transaction
paramsNoStatic parameters passed to the action
enabledNoDefault 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
      }
    }
  ]
}
FieldRequiredDescription
codeYesUnique identifier within the module
entityYesEntity to watch
eventYesEvent type
conditionNoFormula — only fire if true
endpointUrlNoHTTP endpoint to POST to (can be set in the admin UI later)
secretCdNoReference to a secret used for HMAC-SHA256 signing
payload.includeNoArray of fields to include — omit for all
payload.includeOldNotrue to include old values for update/delete
enabledNoDefault 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

ScenarioUse
Notify a user when a record changesTrigger → action with notify()
Send an email on status changeTrigger → action with sendEmail()
Update a related recordTrigger (sync) → action with field set
Post to Slack/Teams/DiscordWebhook
Sync data to an external CRM/ERPWebhook
Run complex multi-step business logicTrigger → 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.