This document describes the enhanced features added to make the notification router more dynamic and powerful.
Each webhook can now be configured with a match mode that determines how rules are evaluated when multiple rules match the incoming webhook payload.
- Behavior: Stops after the first rule that matches conditions
- Use Case: When you want only one action to trigger, prioritized by rule priority
- Example: Alert escalation where you want the highest priority matching rule to trigger
{
"matchMode": "first_match"
}- Behavior: Evaluates ALL rules and triggers every rule that matches
- Use Case: When you want multiple independent notifications for the same event
- Example: Server monitoring where you want to notify different teams through different channels
{
"matchMode": "all_matches"
}POST /api/webhooks
Content-Type: application/json
{
"name": "Multi-Team Webhook",
"description": "Sends to all matching teams",
"matchMode": "all_matches",
"enabled": true
}PATCH /api/webhooks/{webhookId}
Content-Type: application/json
{
"matchMode": "first_match"
}- When a webhook receives a payload, it evaluates rules in priority order (highest to lowest)
- In
first_matchmode:- Stops after the first matching rule
- Executes that rule's actions
- Respects debounce settings
- In
all_matchesmode:- Continues evaluating all rules
- Executes all matching rules' actions
- Skips debounced rules but continues checking others
- Returns comma-separated rule IDs in the response
First Match Mode:
Priority 100: If severity == "critical" → Alert on-call engineer
Priority 50: If severity == "high" → Alert team lead
Priority 10: If severity == "medium" → Send to team channel
Input: { "severity": "high" }
Result: Only team lead is alerted (priority 50)
All Matches Mode:
Priority 100: If server == "prod" → Alert DevOps team
Priority 50: If service == "api" → Alert Backend team
Priority 10: If region == "us-east" → Alert Regional team
Input: { "server": "prod", "service": "api", "region": "us-east" }
Result: All three teams are alerted
Correlation rules allow you to trigger actions when multiple webhooks are received from different sources within a specified time window. This is perfect for scenarios like:
- Waiting for confirmations from multiple systems
- Detecting coordinated events across services
- Multi-step workflow tracking
A rule that defines:
- Source Webhook: The first webhook that initiates the correlation
- Target Webhook: The second webhook to wait for
- Time Window: How long to wait for the target webhook (in milliseconds)
- Actions: What to do when both webhooks are received
- Timeout Actions (optional): What to do if the target webhook never arrives
An active tracking record created when the source webhook is received. It:
- Stores the source webhook payload
- Waits for the target webhook within the time window
- Tracks status:
waiting,completed, ortimeout
POST /api/correlations
Content-Type: application/json
{
"name": "Server A + Server B Sync Check",
"description": "Alert if both servers report online within 5 minutes",
"sourceWebhookId": "webhook-server-a-id",
"targetWebhookId": "webhook-server-b-id",
"timeWindowMs": 300000,
"actions": {
"channelIds": ["slack-channel-id"],
"messageTemplate": "✅ Both servers are synchronized!\\n\\nServer A: {{source.server}}\\nServer B: {{target.server}}\\nTime elapsed: {{_correlation.timeElapsed}}ms",
"titleTemplate": "Sync Successful",
"priority": "normal"
},
"timeoutActions": {
"channelIds": ["slack-channel-id"],
"messageTemplate": "⚠️ Server B did not respond within 5 minutes!\\n\\nServer A: {{source.server}}\\nExpected: {{_correlation.expectedTargetWebhookId}}",
"titleTemplate": "Sync Timeout",
"priority": "high"
},
"enabled": true
}GET /api/correlationsGET /api/correlations/{correlationId}PUT /api/correlations/{correlationId}
Content-Type: application/json
{
"timeWindowMs": 600000,
"enabled": false
}DELETE /api/correlations/{correlationId}GET /api/correlations/{correlationId}/states?status=waiting&limit=50&offset=0When a correlation completes (or times out), the action receives a special payload structure:
{
"source": {
// Full payload from source webhook
},
"target": {
// Full payload from target webhook
},
"_correlation": {
"ruleId": "correlation-rule-id",
"ruleName": "Server A + Server B Sync Check",
"sourceReceivedAt": "2026-01-23T10:00:00.000Z",
"targetReceivedAt": "2026-01-23T10:02:30.000Z",
"timeElapsed": 150000
}
}{
"source": {
// Full payload from source webhook
},
"_correlation": {
"ruleId": "correlation-rule-id",
"ruleName": "Server A + Server B Sync Check",
"sourceReceivedAt": "2026-01-23T10:00:00.000Z",
"timeoutAt": "2026-01-23T10:05:00.000Z",
"expectedTargetWebhookId": "webhook-server-b-id"
}
}Scenario: Deploy to staging, wait for health check
- Source: Deployment webhook from CI/CD
- Target: Health check webhook from monitoring
- Window: 10 minutes
- Action: Notify team that deployment is healthy
- Timeout: Alert team that health check failed
Scenario: Primary and backup servers must both come online
- Source: Primary server online webhook
- Target: Backup server online webhook
- Window: 5 minutes
- Action: Notify that cluster is fully operational
- Timeout: Alert that backup server is not responding
Scenario: Payment initiated, wait for payment gateway confirmation
- Source: Payment request webhook
- Target: Payment confirmation webhook
- Window: 2 minutes
- Action: Send receipt to customer
- Timeout: Alert team of payment timeout
-
Source Webhook Received:
- Correlation engine checks for any correlation rules with this webhook as the source
- Creates a
CorrelationStaterecord with statuswaiting - Sets expiration time based on
timeWindowMs
-
Target Webhook Received:
- Correlation engine checks for any
waitingcorrelations expecting this webhook - Updates the correlation state to
completed - Triggers the correlation actions with combined payload
- Correlation engine checks for any
-
Timeout Handling:
- Background cleanup process checks for expired correlations
- Changes status from
waitingtotimeout - Triggers timeout actions if configured
-
Automatic Cleanup:
- Expired correlations are automatically cleaned up
- Old correlation states can be archived or deleted
The existing webhook notification channel enables you to chain multiple notification routers together, creating a distributed notification network.
- Geographic Distribution: Forward notifications to regional routers
- Service Segregation: Route notifications between different service domains
- Escalation Chains: Forward unhandled notifications to higher-tier routers
- Load Distribution: Spread notification processing across multiple instances
On the sending router, create a webhook notification channel pointing to the receiving router:
POST /api/channels
Content-Type: application/json
{
"name": "Regional Router - EU",
"type": "webhook",
"config": {
"url": "https://eu-router.example.com/api/webhook/abc123xyz",
"method": "POST",
"headers": {
"Authorization": "Bearer secret-token",
"X-Source-Router": "us-central"
}
},
"enabled": true
}On the sending router, create a rule that forwards matching notifications:
POST /api/webhooks/{webhookId}/rules
Content-Type: application/json
{
"name": "Forward EU Traffic",
"conditions": {
"logic": "AND",
"conditions": [
{
"field": "region",
"operator": "equals",
"value": "eu"
}
]
},
"actions": {
"channelIds": ["webhook-channel-eu-id"],
"messageTemplate": "{{originalPayload}}",
"priority": "normal"
},
"enabled": true
}The receiving router processes the forwarded webhook like any other webhook:
- Evaluates its own rules
- Can forward to additional routers
- Can apply region-specific logic
One router distributes to multiple downstream routers:
┌─────────────┐
│ Main │
│ Router │
└──────┬──────┘
│
┌────────────┼────────────┐
│ │ │
┌────▼───┐ ┌────▼───┐ ┌────▼───┐
│ Region │ │ Region │ │ Region │
│ US │ │ EU │ │ APAC │
└────────┘ └────────┘ └────────┘
Unhandled notifications escalate through tiers:
┌─────────┐ Timeout ┌─────────┐ Timeout ┌─────────┐
│ Tier 1 │────────────>│ Tier 2 │────────────>│ Tier 3 │
│ Router │ │ Router │ │ Router │
└─────────┘ └─────────┘ └─────────┘
Multiple routers forward to a central aggregator:
┌─────────┐
│ Service │───┐
│ A │ │
└─────────┘ │
│ ┌─────────────┐
┌─────────┐ │ │ Central │
│ Service │───┼───>│ Aggregator │
│ B │ │ │ Router │
└─────────┘ │ └─────────────┘
│
┌─────────┐ │
│ Service │───┘
│ C │
└─────────┘
When chaining routers, prevent infinite loops by:
-
Adding Source Headers:
{ "headers": { "X-Source-Router": "router-a", "X-Hop-Count": "1" } } -
Checking Headers in Conditions:
{ "logic": "AND", "conditions": [ { "field": "_metadata.sourceIp", "operator": "not_equals", "value": "10.0.1.100" } ] } -
Using Hop Limits:
- Track hop count in forwarded payloads
- Reject notifications exceeding a hop limit
# Create webhook channel to Router B
POST /api/channels
{
"name": "Forward to Router B",
"type": "webhook",
"config": {
"url": "https://router-b.example.com/api/webhook/xyz789",
"method": "POST",
"headers": {
"X-Source": "router-a",
"X-Hop": "1"
}
}
}
# Create rule to forward high-priority alerts
POST /api/webhooks/{webhookId}/rules
{
"name": "Forward High Priority",
"conditions": {
"logic": "AND",
"conditions": [
{
"field": "priority",
"operator": "equals",
"value": "high"
},
{
"field": "_metadata.sourceIp",
"operator": "not_equals",
"value": "192.168.1.50"
}
]
},
"actions": {
"channelIds": ["webhook-channel-b"],
"messageTemplate": "{{originalMessage}}",
"priority": "high"
}
}Router B receives the forwarded webhook and can:
- Process it with its own rules
- Forward to Router C if needed
- Apply local notification channels
Existing webhooks will automatically use first_match mode (backward compatible).
To enable all_matches mode:
PATCH /api/webhooks/{webhookId}
{
"matchMode": "all_matches"
}Run the Prisma migration to add the new fields:
npx prisma migrate dev --name add_correlation_and_match_modeThis will:
- Add
matchModefield to Webhook model - Create
CorrelationRulemodel - Create
CorrelationStatemodel
- Create two webhooks (source and target)
- Create a correlation rule linking them
- Send a payload to the source webhook
- Check correlation states:
GET /api/correlations/{id}/states - Send a payload to the target webhook within the time window
- Verify the correlation action was triggered
GET /api/webhooks- List webhooksPOST /api/webhooks- Create webhook (includematchMode)GET /api/webhooks/{id}- Get webhookPATCH /api/webhooks/{id}- Update webhook (includematchMode)DELETE /api/webhooks/{id}- Delete webhook
GET /api/correlations- List correlation rulesPOST /api/correlations- Create correlation ruleGET /api/correlations/{id}- Get correlation rulePUT /api/correlations/{id}- Update correlation ruleDELETE /api/correlations/{id}- Delete correlation ruleGET /api/correlations/{id}/states- Get correlation states
POST /api/channels- Create channel (type:webhookfor router chaining)
- Use
first_matchfor escalation scenarios - Use
all_matchesfor multi-team notifications - Always set clear rule priorities in
first_matchmode
- Keep time windows reasonable (1-10 minutes)
- Always configure timeout actions for production systems
- Monitor correlation states to detect issues
- Use descriptive names for correlation rules
- Include source identifiers in forwarded webhooks
- Implement hop count limits to prevent infinite loops
- Use authentication headers between routers
- Monitor forwarding latency and failure rates
Check:
- Both webhooks are enabled
- Correlation rule is enabled
- Time window is sufficient
- Source webhook was received first
- Check correlation states for errors
Cause: Webhook is set to all_matches mode
Solution:
PATCH /api/webhooks/{id}
{
"matchMode": "first_match"
}Check:
- Target webhook URL is correct
- Target webhook is enabled
- Network connectivity between routers
- Authentication headers are correct
- Check webhook logs for errors
For issues or questions:
- Check webhook logs:
GET /api/webhooks/{id}/logs - Check correlation states:
GET /api/correlations/{id}/states - Review rule conditions and priorities
- Test with simple payloads first