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

OperatorExampleDescription
eqstatus eq 'active'Exact equality
neqrole neq 'admin'Not equal
gtspend gt 100Greater than (numeric)
gtescore gte 650Greater than or equal
ltage lt 18Less than
lterisk lte 0.5Less than or equal
containsmsg contains 'urgent'Case-insensitive substring
not_containsemail not_contains 'spam'Negated substring
starts_withtool starts_with 'send_'Prefix match
ends_withfile ends_with '.sql'Suffix match
matchesphone matches '\d+'Regex match
inenv in listMembership in list
not_inrole not_in listNot in list
is_trueapproved is_trueBoolean truthy
is_falseblocked is_falseBoolean falsy
is_emptynotes is_emptyNull 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.