← Back to documentation

Routing Filters

Use routing filters to send only matching requests to each output target.

6 min read

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:

  1. PayloadRelay evaluates each filter in the list against the post-transform payload, headers, and query parameters.
  2. All filters must match (AND semantics). If any filter fails, the output is skipped.
  3. Outputs that are skipped write a SKIPPED_BY_ROUTING entry 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 push events to Slack and pull_request events to a webhook — create one output to your Slack target with body.action EQUALS push and another output to your webhook target with body.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 IN with a comma-separated list, or REGEX with an alternation pattern such as ^(push|opened)$).

Field Path Syntax

Routing filters evaluate a field path that specifies where to look for the value:

SyntaxExampleResolves to
bodybodyEntire JSON body as text
body.<path>body.event_typeA field in the JSON body
body.<path>.<nested>body.repository.nameNested field access
body.<path>.<n>body.items.0.nameArray element by index (zero-based)
headers.<name>headers.x-github-eventInbound header (case-insensitive lookup)
query.<name>query.sourceQuery 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

OperatorDescription
EQUALSField value equals the comparison value (exact match)
NOT_EQUALSField value does not equal the comparison value
CONTAINSField value contains the comparison value as a substring
NOT_CONTAINSField value does not contain the comparison value
STARTS_WITHField value starts with the comparison value
ENDS_WITHField value ends with the comparison value
INField value is one of a comma- or newline-separated list
NOT_INField value is none of the list
EXISTSField exists and is non-null
NOT_EXISTSField does not exist or is null
GREATER_THANField value (coerced to number) is greater than the comparison value
LESS_THANField value (coerced to number) is less than the comparison value
REGEXField value matches the regular expression (partial match — uses find())
NOT_REGEXField 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.
  • comparisonValue maximum 1024 characters.
  • fieldPath maximum 512 characters.

Examples

Only forward GitHub push events

Code Example
{
  "fieldPath": "headers.x-github-event",
  "operator": "EQUALS",
  "comparisonValue": "push",
  "caseSensitive": false
}

Only forward high-severity alerts

Code Example
{
  "fieldPath": "body.severity",
  "operator": "IN",
  "comparisonValue": "error,critical"
}

Forward when a numeric priority exceeds a threshold

Code Example
{
  "fieldPath": "body.priority",
  "operator": "GREATER_THAN",
  "comparisonValue": "5"
}

Route emails from a specific domain

Code Example
{
  "fieldPath": "body.email",
  "operator": "REGEX",
  "comparisonValue": ".+@example\\.com$"
}

Skip test events

Code Example
{
  "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.