Writing rules
The current rules UI edits rule expressions as raw JSON. This is an important constraint: operators must provide the backend AST shape directly.
What a rule does
A rule is a conditional statement:
If this condition is true, return this outcome.
In practice, rules help you express policies such as:
- observe suspicious activity without blocking it yet
- challenge medium-risk traffic
- block high-confidence bad behavior
Rule definition fields
Every rule definition in the current model includes:
rule_idtenant_idnamedescriptionenabledpriorityexpressionoutcome
Supported outcomes:
AllowObserveChallengeBlock
What each rule field means
| Field | Plain-English meaning | Why you care |
|---|---|---|
rule_id | The system-generated identifier for the rule | Useful when editing, auditing, or debugging a specific rule |
tenant_id | The workspace this rule belongs to | Keeps rules scoped to one tenant |
name | Short human-friendly rule title | Should make sense in a list view |
description | What the rule is trying to catch | Helps others understand intent without reading JSON |
enabled | Whether the backend should evaluate the rule | The current UI always submits true |
priority | Relative ordering weight | Helps decide which rules should run first or get attention first |
expression | The actual condition written as JSON | This is the core logic |
outcome | What Esper returns when the condition matches | Determines the practical effect of the rule |
Choosing an outcome
| Outcome | Best use | Risk level |
|---|---|---|
Observe | Early rollout, learning mode, analytics | Lowest |
Allow | Explicitly permit a known-safe pattern | Low |
Challenge | Ask for stronger verification on suspicious traffic | Medium |
Block | Stop a pattern you trust is harmful | Highest |
Supported expression nodes
The current RuleExpression union supports these node types:
AndOrNotFieldCmpStateCmpWindowCmpFieldExists
Comparison operators:
EqNeLtLeGtGe
State scopes:
EntitySession
Current window counter support:
EventCount
How to read the expression types
| Expression type | Plain-English meaning |
|---|---|
FieldCmp | Compare a value in the current event |
StateCmp | Compare a saved state value from the entity or session |
WindowCmp | Compare a count inside a recent time window |
FieldExists | Check whether a field is present |
And | All child conditions must be true |
Or | At least one child condition must be true |
Not | Reverse the meaning of a child condition |
Example: exact JSON shape
This is the same shape used as the default example in the frontend:
{
"FieldCmp": {
"field_name": "email",
"operator": "Eq",
"value": "flag@example.com"
}
}
More examples
Block when an entity-scoped event count exceeds a threshold:
{
"WindowCmp": {
"scope": "Entity",
"counter": "EventCount",
"window_seconds": 300,
"operator": "Gt",
"value": "5"
}
}
Challenge when a required field exists and a state value mismatches:
{
"And": {
"expressions": [
{
"FieldExists": {
"field_name": "email"
}
},
{
"StateCmp": {
"scope": "Session",
"field_name": "risk_bucket",
"operator": "Ne",
"value": "low"
}
}
]
}
}
Frontend API contract
GET /tenants/{tenant_id}/rules
POST /tenants/{tenant_id}/rules
PATCH /tenants/{tenant_id}/rules/{rule_id}
DELETE /tenants/{tenant_id}/rules/{rule_id}
Current operational note
The current UI always submits enabled: true when creating or updating a rule.
There is no exposed enabled/disabled toggle in the form yet.
Writing rules well
- Start with
Observe, notBlock. - Give each rule a description that explains business intent, not just the data shape.
- Keep field names stable between encoding and rules.
- Prefer one clear rule over one oversized rule with many unrelated conditions.