Semantius Logo
DocsReferenceBusiness LogicJsonLogic Extensions

JsonLogic Extensions

Semantius extends the standard JsonLogic spec with additional variables and operators tailored to record-level computed fields, validation rules, and row-level select rules. This page documents only the extensions; all standard operators (if, and, or, var, ==, +, map, filter, reduce, etc.) work as defined upstream.


Built-in variables

These variables are automatically available inside computed_fields, validation_rules, and select_rule expressions. Access them with the standard var operator, e.g. {"var": "$now"}.

VariableDescription
$todayThe current server date (no time component). Useful for date comparisons such as “expires before today”.
$nowThe current server timestamp. Useful for stamping fields like submitted_at or comparing against a deadline.
$user_idThe id of the currently authenticated user. null when the expression runs outside a user session (for example during a background job).
$oldA snapshot of the record’s previous state, available on update and delete (where it holds the row being removed). null on insert. Use it to compare new vs. old values, e.g. {"!=": [{"var": "status"}, {"var": "$old.status"}]}.
$modeThe operation being performed: "insert", "update", or "delete". Use it to scope a rule to a particular operation — for example, forbid deletion with {"!=": [{"var": "$mode"}, "delete"]}.

Built-in variables are scoped to the record being evaluated. They are not written back to the record, they exist only for the duration of the expression.

Availability by context

computed_fields and validation_rules run during a mutation and see all five variables. select_rule runs during a read and sees only $today, $now, and $user_id. $old and $mode are not bound in a select rule — a read has no previous image and is not a mutation, so they would be meaningless there.

Evaluation on delete

validation_rules also run on delete, evaluated against the row being removed (bound as the root context, with $old set to that same row and $mode equal to "delete"). A rule that evaluates falsy aborts the delete, so a rule such as {"!=": [{"var": "$mode"}, "delete"]} forbids deletion outright. computed_fields still run on delete, but their output is discarded — there is no surviving row to write it to.


Custom operators

Bind a local variable: let

Evaluates an expression once and exposes the result under a name for the body that follows.

Form: {"let": ["<name>", <value>, <body>]}

{
  "let": [
    "total",
    {"+": [{"var": "subtotal"}, {"var": "tax"}]},
    {">": [{"var": "total"}, 100]}
  ]
}

Use it to avoid recomputing the same sub-expression multiple times or to make complex rules more readable.


Fetches an entire record from another entity by its id and binds it to a name, then evaluates the body with that record available.

Form: {"set_record": ["<name>", "<entityName>", <idExpression>, <body>]}

{
  "set_record": [
    "customer",
    "customers",
    {"var": "customer_id"},
    {"==": [{"var": "customer.status"}, "active"]}
  ]
}

The lookup runs through the same access boundary as a normal read. The bound name resolves to null when the referenced record does not exist or when the current user does not have permission to view it. These two cases are deliberately indistinguishable, so a rule cannot use set_record to probe whether a record the caller is not allowed to see exists. Use dotted paths in var to read fields of the loaded record (e.g. {"var": "customer.email"}).


Check the current user’s permission: has_permission

Returns true when the current user holds the named permission, false otherwise. Does not throw.

Form: {"has_permission": "<permission_name>"}

{"if": [
  {"has_permission": "orders.approve"},
  "approved",
  "pending_review"
]}

Enforce a permission: require_permission

Returns true when the current user holds the named permission, and raises an authorization error otherwise. Use this in validation rules to forbid an action outright.

Form: {"require_permission": "<permission_name>"}

{"require_permission": "orders.delete"}

Check the current user’s RACI role: is_raci_actor

Returns true when the current user holds a role assigned the given RACI letter for the process that governs an (entity, to_state) transition, and false otherwise (including when no user is authenticated). Does not throw.

The RACI letter is one of responsible, accountable, consulted, or informed. The first two arguments name the governed table and the target lifecycle state; together they select the process gate, and the operator then checks the current user’s roles against that process’s RACI matrix.

Form: {"is_raci_actor": ["<entity>", "<to_state>", "<letter>"]}

{"if": [
  {"is_raci_actor": ["orders", "approved", "accountable"]},
  "approved",
  {"throw_error": "Only the accountable approver can approve this order"}
]}

Use it in validation_rules to gate a transition on the caller’s RACI role. It is the governance analog of has_permission for processes that are driven by a RACI matrix rather than a flat permission name.


Require that a RACI consultation occurred: has_consultation

The companion to is_raci_actor, this operator enforces the C (Consulted) letter of RACI. Where is_raci_actor checks who the caller is, has_consultation checks whether the Consulted parties have actually been consulted before a record is allowed to move forward.

Returns true when an acted consulted event already exists for the record under an (entity, to_state) gate, that is, a raci_events row with raci = 'consulted' and status = 'acted'. Returns false otherwise. Does not throw.

This backs C-block gates: a record may not enter the target state until the RACI actors marked consulted (with consult_mode = 'block') have responded. The third argument is the id of the record being gated, usually read from the current row with {"var": "id"}.

Form: {"has_consultation": ["<entity>", "<to_state>", <record_id>]}

{"if": [
  {"has_consultation": ["orders", "approved", {"var": "id"}]},
  true,
  {"throw_error": "Required consultation has not been completed"}
]}

Use it in validation_rules to block a transition until the consultation it depends on has been recorded.


Detect a field change on update: value_changed

Returns true when the named field’s value differs from its previous value on update, and true on insert (treating a new record as “changed”). Returns false only when the field’s value is the same as before.

Form: {"value_changed": "<field_name>"}

{"if": [
  {"value_changed": "status"},
  {"throw_error": "Status changes require manager approval"},
  true
]}

Concatenate strings with SQL semantics: concat

Concatenates all arguments into a single string. Unlike the standard cat operator, concat treats null arguments as empty strings (mirroring SQL CONCAT) and serializes non-string values (numbers, booleans, arrays, objects) to their JSON text form.

Form: {"concat": [<value1>, <value2>, ...]}

{"concat": ["Order #", {"var": "id"}, " for ", {"var": "customer_name"}]}

Choose concat when some inputs may be null and you want them to disappear rather than produce the string "null".


Test a string against a regular expression: is_match

Tests whether a string value matches a regular expression pattern. Returns true on a match, false otherwise. null values always return false.

Form: {"is_match": [<value>, "<pattern>"]}

{"is_match": [{"var": "email"}, "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"]}

Useful in validation rules for format checks (email, phone, postal code, SKU prefix) without needing a separate constraint mechanism.


Raise a validation error: throw_error

Aborts the current operation with the given error message. Typically used as a branch of an if expression to enforce a business rule with a custom message.

Form: {"throw_error": "<message>"}

{"if": [
  {"<": [{"var": "quantity"}, 1]},
  {"throw_error": "Quantity must be at least 1"},
  true
]}

The message surfaces to the caller as a constraint-violation error.