A rule is a small declarative expression that runs against every event flowing through your project. Rules decide what to do before anything reaches your agent or your endpoints:
- Drop noisy events (bot PRs, test-mode webhooks, health-check pings).
- Transform fields in the event’s data block — redact secrets, lowercase emails, trim whitespace.
- Route matching events to specific endpoints, overriding the default fan-out.
- Tag events for downstream search / filtering.
Rules live in the dashboard at Projects → your project → Rules. They’re declarative — no code, no redeploys — and evaluate fast enough to be invisible in your latency.
Where rules run
Rules fire after Nevo persists the event, before fan-out to SDK clients and HTTP endpoints. That means:
- The stored event keeps the verbatim source envelope in
raw_payload— transforms don’t rewrite audit history. event.datathat your SDK receives reflects whatever transforms matched.- Dropped events are persisted (with
status=dropped) so the dashboard shows exactly which rule killed which event.
Ingest-stage rules (fire before persistence) are planned but not yet enabled in the dashboard.
Match tree
Match against any field in the Nevo event using dotted JSON paths:
{
"all": [
{ "path": "channel.type", "op": "eq", "value": "webhook" },
{ "path": "data.body.action", "op": "in", "value": ["opened", "reopened"] },
{ "any": [
{ "path": "data.body.user.login", "op": "ends_with", "value": "[bot]" },
{ "path": "data.body.pr.title", "op": "matches", "value": "^WIP:" }
]}
]
}
Paths are free-form — anything under data.body for webhooks works
even though webhook payloads have arbitrary shapes. No registered
schema required.
Operators (14)
| Operator | Semantics |
|---|---|
eq / neq | Strict equality. Numbers coerce, strings compare byte-wise. |
contains | Substring (strings) or set membership (arrays). |
not_contains | Inverse. |
starts_with | String prefix. |
ends_with | String suffix. |
in / not_in | Membership in a JSON array value. |
matches | RE2 regex match (max 200 chars to block catastrophic backtracking). |
not_matches | Inverse. |
gt / gte / lt / lte | Numeric compare; string fallback for timestamps / sortable IDs. |
exists / missing | Presence test; does not care about the value. |
Group logic
{ "all": [...] }— AND; every child must match.{ "any": [...] }— OR; at least one child must match.- A bare leaf (
{ path, op, value }) is an implicit single-condition match. - Groups nest arbitrarily. The dashboard’s visual builder covers flat single-group rules; deeper trees fall back to the JSON editor.
Missing paths fail closed. A rule that references data.body.typo
when the field doesn’t exist evaluates to false for everything except
missing. Typos don’t accidentally match every event.
Actions
A rule has exactly one action. Multiple rules matching the same event compose — transforms, routes, and tags accumulate; the first drop short-circuits the rest.
drop
{ "type": "drop" }
Event is persisted with status=dropped, skipped from realtime fan-out,
and no endpoint POSTs fire. The matching rule name is visible in the
event detail panel.
transform
{
"type": "transform",
"ops": [
{ "op": "set", "path": "data.priority", "value": "low" },
{ "op": "redact", "path": "data.text", "regex": "sk-[A-Za-z0-9]{10,}", "replace": "[REDACTED]" },
{ "op": "uppercase", "path": "data.subject" },
{ "op": "lowercase", "path": "data.email" },
{ "op": "trim", "path": "data.note" }
]
}
Transforms mutate only the projected Nevo event — the one SDKs and
endpoints see. raw_payload stays verbatim. Ops apply in order, so
uppercase-then-trim works as expected.
Missing paths are silently skipped (user config shouldn’t crash unrelated events).
route_only
{ "type": "route_only", "endpoint_ids": ["<uuid>", "<uuid>"] }
Overrides the default fan-out — only the listed endpoints receive the event. Useful for triage: send P0 events to the oncall endpoint and normal events everywhere else.
tag
{ "type": "tag", "tags": ["urgent", "p0"] }
Attaches labels to the event. Tags are surfaced on the event record for search and downstream filtering.
Priority + evaluation order
Each rule has a priority (int, lower = higher priority). Rules are
evaluated priority-first, ties broken by rule ID for determinism.
The first matching drop short-circuits the walk — later rules don’t run. Non-drop actions accumulate:
rule A (priority 10): transform → data.title uppercase
rule B (priority 20): tag ["processed"]
rule C (priority 30): drop ← never reached if A + B already matched drop-shaped conditions
Dry-run
Before enabling a rule, run it against historical events. The dashboard gives you 1h / 24h / 7d windows (7d max):
POST /projects/{id}/rules/dry-run
{ "stage": "deliver", "match": {...}, "action": {...}, "windowHours": 24 }
Returns:
matched/scannedcountsbyActionbreakdown (drop: 12,transform: 3, …)- Up to 50 sampled events with:
- the event’s
dataas the rule saw it - for transforms:
afterdiff - for
route_only: the endpoint IDs that would receive it - for
tag: the tags that would be added
- the event’s
Read-only — zero writes, safe to run repeatedly.
Import / export
Rules are portable:
- Export — dashboard → Rules → export downloads all project rules as JSON (name, description, stage, match, action, priority). Server-side fields (id, counts) are stripped.
- Import — upload a JSON file. Entries can be a single object or an array; each is validated individually, malformed entries are skipped with a summary toast.
Use this to copy rules between projects, back them up, or bootstrap a new project from a known-good set.
Templates
The dashboard ships with a curated library of starter rules:
- Drop GitHub bot PRs — silence dependabot / renovate / github-actions noise
- Drop Stripe test events — skip all
livemode=falsewebhooks - Redact API keys in webhook bodies — mask
sk-…andsk_live_…strings - Tag urgent Slack mentions — attach
urgentto messages containing<!here>or<!channel>
Click templates on the rules page to preview + one-click create. The template is just a pre-filled form; tweak before saving.
Plan limits
| Tier | Rules per project |
|---|---|
| Hobby | 1 |
| Hacker | 5 |
| Pro | 25 |
| Scale | unlimited |
Hitting the limit surfaces a 402 in the API with code: quota_exceeded;
the dashboard shows an upgrade prompt.
Propagation
Rule edits in the dashboard apply to incoming events within about a minute. Regex patterns have a 200-character limit — longer patterns are rejected when you save.