Guard DSL
Write rules in plain English. They compile to safe JSON AST automatically. No eval(), no code injection.
Syntax
when <field> <operator> <value> then <action> Rules are deterministic. The LLM never sees them. They run BEFORE every action.
Examples
when action contains 'delete' and env eq 'production' then block
when spend gt 100 then require_approval
when cpu_pct gt 90 then alert
when message contains 'urgent' then escalate
when day_of_week eq 'Friday' and risk gt 0.7 then block_deploy All 16 Operators
| Operator | Example | Description |
|---|---|---|
eq | status eq 'active' | Exact equality |
neq | role neq 'admin' | Not equal |
gt | spend gt 100 | Greater than (numeric) |
gte | score gte 650 | Greater than or equal |
lt | age lt 18 | Less than |
lte | risk lte 0.5 | Less than or equal |
contains | msg contains 'urgent' | Case-insensitive substring |
not_contains | email not_contains 'spam' | Negated substring |
starts_with | tool starts_with 'send_' | Prefix match |
ends_with | file ends_with '.sql' | Suffix match |
matches | phone matches '\d+' | Regex match |
in | env in list | Membership in list |
not_in | role not_in list | Not in list |
is_true | approved is_true | Boolean truthy |
is_false | blocked is_false | Boolean falsy |
is_empty | notes is_empty | Null or blank |
Logical Combinators
AND — all conditions must be true
when role eq 'admin' and mfa is_true and ip_allowed is_true then allow OR — any condition can be true
when env eq 'staging' or env eq 'development' then allow_deploy NOT — negate a condition
when not tool starts_with 'delete' then allow Important details
Operator precedence
and and or evaluate left-to-right with no precedence. a and b or c evaluates as (a and b) or c. Use separate rules instead of complex expressions for clarity.
Field names
Fields can be any key from the context object you pass to guard.check(). Common fields: action, env, tool, spend, amount, role, day_of_week. You define the context — the DSL just reads from it.
Case sensitivity
contains, not_contains are case-insensitive. All other string operators (eq, starts_with, ends_with) are case-sensitive. Use contains when you want flexible matching.
Type coercion
Numeric operators (gt, lt, gte, lte) convert both sides to numbers. String operators compare as strings. eq uses strict string comparison — 100 eq '100' is false.
Using in code
import { createGuard } from '@savants.dev/guard';
const guard = createGuard([
"when action contains 'delete' and env eq 'production' then block",
"when spend gt 100 then require_approval",
]);
// Check any context
const result = guard.check({ action: 'delete_db', env: 'production' });
// result.blocked === true
// result.rule === "when action contains 'delete'..."
// result.action === "block" Try it live: Paste a schema into the Playground and test guard rules interactively.