LANGUAGE REFERENCE • AMI · DSL v2

Trigger DSL

Write deterministic Memrail conditions over state, bounded tags, and event history with reachability, scoping, and retention in mind.

View raw Markdown

An EMU trigger is a boolean expression over typed context. The language is deliberately small so a policy is inspectable, reproducible, and statically analyzable.

Quick reference#

Memrail DSL
state.user.tier == 'premium'
state.user.tier IN ['gold', 'platinum']
state.account.verified == true AND state.account.suspended == false
tag.intent == 'refund_request'
event.agent.sent.email IN 'PT24H'
COUNT event.user.failed.login IN 'PT10M' >= 5
event.order.shipped.package WHERE order_id == 'ord-456' IN 'P7D'
state.user.email ENDS_WITH '@company.com'
state.document.code MATCHES '^[A-Z]{3}-\d{4}$'

Logical and special operators are uppercase. String literals and durations use single quotes. State keys are lowercase and namespaced, such as state.user.tier; a single key such as state.tier is invalid.

State conditions#

Memrail DSL
state.user.tier == 'premium'
state.ticket.priority != 'low'
state.order.total >= 1000
state.account.balance < 0
state.account.verified
NOT state.user.banned

Send numeric state for numeric comparisons. The evaluator also coerces numeric-looking strings, so validate input types before calling it rather than relying on coercion. Ordinary state string equality is case-sensitive.

Boolean shorthand checks truthiness, not a strict boolean contract. Nonzero numbers, including negative numbers, are true. Strings other than '', 'false', 'no', and '0' are true, ignoring case for those four exclusions. Thus 'unknown' is true. For permission-bearing fields, emit actual booleans and use explicit == true or == false.

Compare facts and do arithmetic#

Memrail DSL
state.refund.amount > state.customer.refund_limit
state.request.cost + state.budget.spent <= state.budget.limit

State comparisons can read another state or tag accessor. Numeric arithmetic supports +, -, *, and /; multiplication and division bind more tightly than addition and subtraction. Missing operands and division by zero make the arithmetic comparison false. Invalid nonnumeric arithmetic is an evaluation error, not a successful comparison.

Presence#

Memrail DSL
state.user.email EXISTS
state.refund.request_id EXISTS AND state.refund.amount > 0

EXISTS tests that a value is present and non-null. A positive comparison, including !=, returns false when either referenced value is missing. But the entire trigger does not necessarily become false: NOT flips that result, and an OR branch can independently match.

Memrail DSL
// With user.banned missing, the first expression is true; the second is false.
NOT state.user.banned
state.user.banned EXISTS AND state.user.banned == false

Guard required context before negation and before interpolating an action or event filter. Do not treat absence as an authorization decision.

Membership#

Memrail DSL
state.user.role IN ['admin', 'owner']
state.ticket.priority IN ['high', 'critical']
tag.intent IN ['refund_request', 'charge_dispute']

String membership uses Unicode NFKC normalization, case folding, and trimming. For example, a state value of 'ADMIN' matches IN ['admin'] but does not match == 'admin'. Numeric list membership does not use the numeric-string coercion of comparison operators.

String operations#

Operator Example
STARTS_WITH state.case.reference STARTS_WITH 'EU-'
ENDS_WITH state.user.email ENDS_WITH '@company.com'
CONTAINS state.message.subject CONTAINS 'URGENT'
MATCHES state.document.code MATCHES '^[A-Z]{3}-\d{4}$'

String operations are case-sensitive. MATCHES uses Python regular-expression search semantics; anchor with ^ and $ when the whole value must match. Keep expressions bounded and test representative inputs. The literal \d above is valid in the DSL: the lexer preserves that escape. When putting the trigger inside JSON, escape the backslash as \\d; host-language string literals may require another escaping layer.

Tags#

Tags are classifications or metadata:

Memrail DSL
tag.intent == 'upgrade_request'
tag.sentiment == 'negative'
tag.channel == 'email'

Tag equality normalizes the kind and string value with Unicode NFKC, case folding, and trimming. Keep producer keys consistently lowercase even though normalized equality accepts other casing.

If a model produces a tag, constrain it to a fixed enum before building the ATOM. Define an explicit branch for unknown and label its model provenance; the SDK preserves source metadata on requests. See agent control patterns.

Events#

Modern event predicates require a subject.verb.object name followed by IN and a single-quoted ISO 8601 duration. A bare event name is not a complete predicate:

Memrail DSL
event.user.failed.login IN 'PT15M'
event.customer.completed.purchase IN 'P7D'
NOT event.agent.responded.ticket IN 'PT2H'

Event retention defaults to 90 days and is configurable at the organization level, inherited by workspaces. Events receive a per-document expiry based on their event timestamp and that effective policy. A longer query window can still match recent retained events, but cannot recover expired history; the validator can report WARN-WINDOW-OUT-OF-RETENTION. Confirm the effective retention before relying on a negative history check.

An existence query can name another project in the same workspace with event.project_slug.subject.verb.object IN 'PT1H'. Document this cross-project dependency in your topology. COUNT accepts only the three-token subject.verb.object form and does not support that project prefix.

Count events#

Memrail DSL
COUNT event.user.failed.login IN 'PT15M' >= 5
COUNT event.customer.placed.order IN 'P30D' >= 3

Both COUNT event.user.failed.login IN 'PT15M' >= 5 and COUNT(event.user.failed.login IN 'PT15M') >= 5 are supported. Use one house style consistently. A count requires a comparison against a number; with no matching events, the count is zero, so == 0 is true.

Scope events to the current entity#

Without a WHERE clause, an event query may match any event of that name in the project context. Scope multi-entity policy with event attributes:

Memrail DSL
event.customer.received.refund
  WHERE customer_id == '{{customer.id}}'
  IN 'P30D'

The template literal reads a current state value and converts it to a string. Emit entity identifiers as string attributes so the types match. An unresolved placeholder stays literal and normally matches nothing; under NOT, that produces true. Add an explicit state.customer.id EXISTS guard when the identifier is required. Do not write a state accessor directly as a WHERE value:

Memrail DSL
// Invalid
event.customer.received.refund WHERE customer_id == state.customer.id IN 'P30D'

// Valid
event.customer.received.refund WHERE customer_id == '{{customer.id}}' IN 'P30D'

The event producer must include customer_id in its attributes. Attribute filters support equality conditions joined by AND; values are single-quoted strings, numeric literals, or quoted templates. Numeric range operators and state accessors are not supported as WHERE values:

Memrail DSL
COUNT event.agent.called.tool
  WHERE customer_id == '{{customer.id}}' AND tool_name == 'refund'
  IN 'PT1H' >= 3

Durations#

Literal Window
'PT5M' five minutes
'PT1H' one hour
'PT24H' twenty-four hours
'P1D' one day
'P7D' seven days
'P30D' thirty days
'P90D' ninety days

Use durations only for event history. EMU cooldowns use an object with integer seconds, not an ISO string.

Fractional seconds are preserved: 'PT1.5S' means a 1.5-second window, including an event exactly on its boundary. Fractional hours such as 'PT1.5H' are invalid, so write 'PT1H30M'. Months mean 30 days and years mean 365 days, not calendar-aware intervals.

json
{ "cooldown": { "seconds": 3600, "gate": "ack" } }

Operator precedence#

From highest to lowest: NOT, AND, then OR. Use parentheses whenever mixed operators would force a reviewer to remember precedence.

Memrail DSL
(state.user.tier == 'premium' OR state.user.tier == 'enterprise')
AND state.account.verified

Reachability review#

For every trigger, list the required inputs:

Memrail DSL
state.customer.tier == 'enterprise'
AND state.customer.id EXISTS
AND tag.intent == 'refund_request'
AND NOT event.customer.received.refund
  WHERE customer_id == '{{customer.id}}'
  IN 'P30D'
Dependency Requirement
state.customer.tier supplied at this decision point
tag.intent classifier always returns a declared enum
state.customer.id supplied because the event filter interpolates it
event.customer.received.refund emitted after successful refund with customer_id attribute

The negative event predicate evaluates true when there is no matching event, including when a producer is absent, a filter identifier is missing, or relevant history has expired. Reliable interpretation requires complete producer coverage, required context, and an appropriate retention window.

Common invalid expressions#

Invalid Correct Reason
state.tier == 'premium' state.user.tier == 'premium' state key is not namespaced
state.User.Tier == 'premium' state.user.tier == 'premium' keys must be lowercase
... and ... ... AND ... logical operators are uppercase
state.user.tier == "premium" state.user.tier == 'premium' strings use single quotes
event.user.login IN PT1H event.user.failed.login IN 'PT1H' S.V.O name and quoted duration required
WHERE user_id == state.user.id WHERE user_id == '{{user.id}}' state accessors are invalid as filter values
event.user.created.account event.user.created.account IN 'P7D' modern event predicates require a quoted time window

Complete control example#

Memrail DSL
state.refund.request_id EXISTS
AND state.customer.id EXISTS
AND state.refund.amount > 500
AND tag.refund_intent IN ['service_failure', 'fraud']
AND NOT event.agent.created.refund_review
  WHERE request_id == '{{refund.request_id}}'
  IN 'P7D'

This expression is syntactically valid regardless of which inputs arrive. For its positive conditions to match, the call must supply both IDs, a numeric amount above 500, and a matching tag. For the negative event check to be meaningful, the application must emit agent.created.refund_review with a matching string request_id after the action succeeds.