Routing Filters
Use routing filters to send only matching requests to each output target.
Routing Filters
Routing filters let you control which incoming requests are forwarded to each output, based on the request payload, headers, or query parameters. With routing filters, you can, for example, send only GitHub push events to your Slack channel, while ignoring all other event types.
Outputs with no filters always dispatch every request — fully backward-compatible with existing configurations.
How It Works
Each output (the link between an endpoint and a relay target) can have an optional list of routing filters. When a request arrives:
- PayloadRelay evaluates each filter in the list against the post-transform payload, headers, and query parameters.
- All filters must match (AND semantics). If any filter fails, the output is skipped.
- Outputs that are skipped write a
SKIPPED_BY_ROUTINGentry to the activity log so you can observe what happened.
For OR semantics across different targets, attach the appropriate filter to each target's output independently. Each target gets its own output row, evaluated independently, so a single inbound request can fan out to several targets when each target's filter set matches.
Example: Send GitHub
pushevents to Slack andpull_requestevents to a webhook — create one output to your Slack target withbody.action EQUALS pushand another output to your webhook target withbody.action EQUALS opened. The two outputs are evaluated independently for every inbound request.
Note: Each endpoint may have at most one output per relay target. To dispatch the same payload to one target under multiple disjoint conditions, combine the conditions into a single filter set (for example, use
INwith a comma-separated list, orREGEXwith an alternation pattern such as^(push|opened)$).
Field Path Syntax
Routing filters evaluate a field path that specifies where to look for the value:
| Syntax | Example | Resolves to |
|---|---|---|
body | body | Entire JSON body as text |
body.<path> | body.event_type | A field in the JSON body |
body.<path>.<nested> | body.repository.name | Nested field access |
body.<path>.<n> | body.items.0.name | Array element by index (zero-based) |
headers.<name> | headers.x-github-event | Inbound header (case-insensitive lookup) |
query.<name> | query.source | Query parameter |
Use dot notation for arrays (body.items.0.name). Bracket notation (body.items[0].name) is not accepted.
If the field doesn't exist in the payload (for example, the path doesn't resolve), the behavior depends on the operator: EXISTS returns false, EQUALS / comparison operators return false, and negation operators such as NOT_EXISTS, NOT_EQUALS, NOT_CONTAINS, NOT_IN, and NOT_REGEX return true. Add an EXISTS filter when a negated comparison should only pass for requests that actually include the field.
JSON null is treated the same as a missing field. Body values are coerced to text before comparison; object and array values are compared as compact JSON strings. For non-JSON requests that cannot be parsed as JSON, body.* paths behave like missing fields. Repeated query parameters are joined with commas before query.* filters evaluate them.
Supported Operators
| Operator | Description |
|---|---|
EQUALS | Field value equals the comparison value (exact match) |
NOT_EQUALS | Field value does not equal the comparison value |
CONTAINS | Field value contains the comparison value as a substring |
NOT_CONTAINS | Field value does not contain the comparison value |
STARTS_WITH | Field value starts with the comparison value |
ENDS_WITH | Field value ends with the comparison value |
IN | Field value is one of a comma- or newline-separated list |
NOT_IN | Field value is none of the list |
EXISTS | Field exists and is non-null |
NOT_EXISTS | Field does not exist or is null |
GREATER_THAN | Field value (coerced to number) is greater than the comparison value |
LESS_THAN | Field value (coerced to number) is less than the comparison value |
REGEX | Field value matches the regular expression (partial match — uses find()) |
NOT_REGEX | Field value does not match the regular expression |
Case sensitivity
String operators (EQUALS, NOT_EQUALS, CONTAINS, NOT_CONTAINS, STARTS_WITH, ENDS_WITH, IN, NOT_IN) have an optional case-sensitive toggle (default: enabled). Uncheck it to do case-insensitive comparisons.
Numeric operators
GREATER_THAN and LESS_THAN attempt to parse the field value as a number. If the value is non-numeric (for example, a string like "critical"), the filter does not match.
Regular expressions
REGEX and NOT_REGEX use Java regular expression syntax. The comparison is a partial match — the pattern is found anywhere in the value. Patterns are validated at save time; an invalid regex returns a 400 error.
Limits
- Maximum 20 filters per output.
comparisonValuemaximum 1024 characters.fieldPathmaximum 512 characters.
Examples
Only forward GitHub push events
{
"fieldPath": "headers.x-github-event",
"operator": "EQUALS",
"comparisonValue": "push",
"caseSensitive": false
}Only forward high-severity alerts
{
"fieldPath": "body.severity",
"operator": "IN",
"comparisonValue": "error,critical"
}Forward when a numeric priority exceeds a threshold
{
"fieldPath": "body.priority",
"operator": "GREATER_THAN",
"comparisonValue": "5"
}Route emails from a specific domain
{
"fieldPath": "body.email",
"operator": "REGEX",
"comparisonValue": ".+@example\\.com$"
}Skip test events
{
"fieldPath": "body.environment",
"operator": "NOT_EQUALS",
"comparisonValue": "test"
}Observability
When a routing filter excludes an output, PayloadRelay writes a SKIPPED_BY_ROUTING delivery-status row to the activity log. You can see this in the Activity page by expanding any request row and looking at the delivery statuses for each output. The reason string identifies the configured filter that did not match — for example, "filter 'body.event_type EQUALS push' did not match" or "filter 'body.severity EXISTS' did not match: field does not exist".
Privacy note: Reason strings deliberately never include the resolved payload value. Request payload bodies are not retained in activity logs, and the reason string preserves that contract. To inspect the actual incoming value, use the request-level fields shown on the activity row or replay the request against your endpoint with verbose logging on your receiver.
Test Button Behavior
The relay-target test button (on the Targets page) sends a synthetic payload directly to the target and bypasses all routing filters. This is intentional — test payloads rarely match your production filter conditions, and you want to confirm the target itself is reachable. The response includes "routingFiltersBypassed": "true" to make this explicit.
If you need to test that your filters work correctly, send a real request to the endpoint relay URL and observe the activity log.
Troubleshooting
My output is always skipped. Check the activity log for the SKIPPED_BY_ROUTING reason. The reason string identifies which configured filter failed (the field path, operator, and configured comparison value). Common causes: wrong field path, wrong operator, case mismatch, or a nested JSON path that doesn't exist. Because the actual incoming value isn't included in the reason (see the privacy note above), replay the request against a debug receiver if you need to compare against the live payload.
My regex doesn't match. REGEX uses Java regex syntax with partial (find) matching — you don't need anchors (^ / $) unless you want them. Remember to escape special characters (\., \d, etc.).
My numeric comparison fails. GREATER_THAN / LESS_THAN require the field to be numeric. If the field value is "5" (a JSON string), it still coerces to 5. But if the field is "high", the comparison fails. Use EQUALS/IN for string-valued severity levels.
I want OR logic on a single target. Each endpoint may have only one output per target, so combine the conditions into one filter set: prefer IN with a comma-separated list, or REGEX with an alternation (e.g., ^(push|opened)$). To OR across different targets, attach the relevant filter to each target's output — every output is evaluated independently.