Skip to main content
Version: 0.1.0

REL (Raisin Expression Language)

REL is a lightweight, safe expression language for evaluating conditions throughout RaisinDB. It is used in permission conditions, flow decision nodes, container rules, and the admin console condition builder.

Where REL Is Used

ContextExample
Access control conditionsnode.created_by == auth.local_user_id
Flow decision stepsinput.priority >= 5 || input.urgent == true
Flow container rulesinput.region == 'eu'
Admin console condition builderVisual UI compiled to REL (via WASM)

Data Types

TypeExamplesNotes
NullnullRepresents absence of a value
Booleantrue, false
Integer42, -7, 064-bit signed integer
Float3.14, -0.564-bit floating point
String'hello', "world"Single or double quotes
Array[1, 2, 3], ['a', 'b']Heterogeneous elements
Object{key: 'value', count: 42}String keys, any values

Operators

Precedence (lowest to highest)

PrecedenceOperatorDescription
1||Logical OR (short-circuit)
2&&Logical AND (short-circuit)
3== != < > <= >= RELATESComparison and graph traversal
4+ -Addition, subtraction, string concatenation
5* / %Multiplication, division, modulo
6! - (unary)Logical NOT, numeric negation
7. [] .method()Property access, index access, method calls

Use parentheses () to override precedence.

Comparison

input.value == 42
input.status != 'archived'
input.count > 10
input.score < 100
input.priority >= 5
input.amount <= 1000

Equality (==, !=) works across all types. Integers and floats compare cross-type (42 == 42.0 is true). Arrays and objects compare by deep equality.

Ordering (<, >, <=, >=) works on integers, floats, and strings. Strings compare lexicographically. Comparing incompatible types (e.g., string to integer) produces an error.

Logical

input.active == true && input.verified == true
input.admin == true || input.moderator == true
!input.disabled

&& and || use short-circuit evaluation: the right operand is not evaluated if the left operand determines the result.

Arithmetic

input.price * input.quantity
input.total / input.count
input.score % 10
input.base + input.bonus
input.balance - input.withdrawal

When mixing integers and floats, the result is promoted to float (5 + 3.14 returns 8.14). Division by zero produces an error.

The + operator also concatenates strings:

'hello' + ' ' + 'world'     // "hello world"

Unary

!input.disabled              // logical NOT
-input.value // numeric negation

! converts the operand to a boolean via truthiness and negates it. - works on integers and floats only.


Property Access

Access variables and nested properties with dot notation:

input.orderId
input.user.email
context.variables.result
auth.local_user_id

Index Access

Access array elements by index and object properties by key:

input.items[0]
input.items[0].price
data["key"]

Negative indices produce an error. Out-of-bounds indices produce an error.

Null Safety

Property access and method calls on null return null instead of an error, similar to JavaScript's optional chaining (?.):

input.user.name              // returns null if input.user is null
input.name.toLowerCase() // returns null if input.name is null

This means you can safely chain property accesses without explicit null checks. Use && short-circuit evaluation for conditional access:

input.meta && input.meta.published == true

Methods

All methods use dot-call syntax: value.method(args). Methods called on null return null (null-safe).

Universal Methods

These work on strings, arrays, and objects.

MethodSignatureReturnsDescription
lengthvalue.length()IntegerNumber of characters, elements, or keys
isEmptyvalue.isEmpty()Booleantrue if null, empty string, empty array, or empty object
isNotEmptyvalue.isNotEmpty()BooleanNegation of isEmpty()
input.name.length()          // 5 for "hello"
input.tags.length() // 3 for ['a', 'b', 'c']
input.title.isEmpty() // true for ""
input.items.isNotEmpty() // true for [1, 2]

length() on null returns 0.

Polymorphic: contains

MethodSignatureReturnsDescription
containsstring.contains(substring)BooleanCheck if string contains a substring
containsarray.contains(element)BooleanCheck if array contains an element (deep equality)
input.name.contains('test')         // substring check
input.tags.contains('urgent') // array element check
auth.roles.contains('editor') // role membership check

String Methods

MethodSignatureReturnsDescription
startsWithstr.startsWith(prefix)BooleanCheck if string starts with prefix
endsWithstr.endsWith(suffix)BooleanCheck if string ends with suffix
toLowerCasestr.toLowerCase()StringConvert to lowercase
toUpperCasestr.toUpperCase()StringConvert to uppercase
trimstr.trim()StringRemove leading and trailing whitespace
substringstr.substring(start)StringExtract from start index to end
substringstr.substring(start, end)StringExtract from start to end index (exclusive)
input.email.endsWith('@example.com')
input.code.startsWith('PRE-')
input.name.trim().toLowerCase()
input.text.substring(0, 10)

Indices in substring are clamped to valid ranges — out-of-bounds values are silently adjusted.

Methods can be chained:

input.name.trim().toLowerCase().contains('test')

Array Methods

MethodSignatureReturnsDescription
firstarr.first()ValueFirst element, or null if empty
lastarr.last()ValueLast element, or null if empty
indexOfarr.indexOf(element)IntegerIndex of element, or -1 if not found
joinarr.join()StringConcatenate elements with no separator
joinarr.join(separator)StringConcatenate elements with separator
input.items.first()
input.items.last()
input.ids.indexOf('abc')
input.names.join(', ') // "alice, bob, carol"
[1, true, 'x'].join('-') // "1-true-x"

join converts non-string elements to their string representation. Nested arrays and objects render as [object].

Path Methods

These methods operate on hierarchical path strings (e.g., /content/blog/post1).

MethodSignatureReturnsDescription
parentpath.parent()StringParent path (one level up)
parentpath.parent(levels)StringAncestor N levels up
ancestorpath.ancestor(depth)StringAncestor at absolute depth from root
depthpath.depth()IntegerNumber of path segments
ancestorOfpath.ancestorOf(other)BooleanIs this path an ancestor of other?
descendantOfpath.descendantOf(other)BooleanIs this path a descendant of other?
childOfpath.childOf(other)BooleanIs this a direct child of other?
'/content/blog/post1'.parent()            // "/content/blog"
'/content/blog/post1'.parent(2) // "/content"
'/content/blog/post1'.ancestor(1) // "/content"
'/content/blog/post1'.ancestor(2) // "/content/blog"
'/content/blog/post1'.depth() // 3
'/content'.ancestorOf('/content/blog') // true
'/content/blog'.descendantOf('/content') // true
'/content/blog'.childOf('/content') // true
'/content/blog/x'.childOf('/content') // false (grandchild)

A path is not considered an ancestor or descendant of itself.


Graph Relationships (RELATES)

The RELATES operator tests whether two nodes are connected through relationships in the graph. It is used in permission conditions and evaluated asynchronously.

Syntax

source RELATES target VIA relation_types [DEPTH min..max] [DIRECTION direction]

Examples

// Single relation type
node.created_by RELATES auth.local_user_id VIA 'FRIENDS_WITH'

// Multiple relation types
node.created_by RELATES auth.local_user_id VIA ['FOLLOWS', 'FRIENDS_WITH']

// With depth (up to 3 hops)
node.created_by RELATES auth.local_user_id VIA 'MANAGES' DEPTH 1..3

// With direction
node.created_by RELATES auth.local_user_id VIA 'REPORTS_TO' DIRECTION OUTGOING

Clauses

ClauseRequiredDefaultDescription
VIAYesRelation type(s) to traverse. Single string or array.
DEPTHNo1..1Min and max traversal hops (inclusive). 1..1 = direct connection only.
DIRECTIONNoANYOUTGOING, INCOMING, or ANY.

Use in Permissions

# Friends can read my profile
- path: "users/**/profile"
operations: ["read"]
condition: "node.created_by RELATES auth.local_user_id VIA 'FRIENDS_WITH'"

# Friends-of-friends see limited fields (2 hops)
- path: "users/**/profile"
operations: ["read"]
fields: ["display_name", "avatar", "bio"]
condition: "node.created_by RELATES auth.local_user_id VIA 'FRIENDS_WITH' DEPTH 1..2"

Truthiness

When a value is used in a boolean context (&&, ||, !, or as a condition result), it is converted to a boolean:

TypeTruthyFalsy
Nullnull
Booleantruefalse
Integernon-zero0
Floatnon-zero0.0
Stringnon-empty""
Arraynon-empty[]
Objectnon-empty{}

Type Coercion

REL performs limited automatic type coercion:

ContextRule
Integer + Float arithmeticResult promoted to Float
Integer == Float comparisonInteger is converted to Float for comparison
String + StringConcatenation (not arithmetic)
!valueConverted via truthiness
&& / || operandsConverted via truthiness, result is Boolean

Incompatible operations produce errors rather than silent coercion. For example, 'hello' > 42 is an error, not a coerced comparison.


Context Variables

REL expressions are evaluated against a context object. The available variables depend on where the expression is used.

Permission Conditions

VariableTypeDescription
node.idStringNode ID being accessed
node.pathStringNode path
node.created_byStringCreator's user ID
auth.local_user_idStringCurrent user's workspace-specific ID
auth.user_idStringCurrent user's global identity ID
auth.homeStringUser's home path (e.g., /users/jane)
auth.emailStringUser's email
auth.rolesArrayEffective role IDs
auth.groupsArrayGroup IDs

Flow Conditions

VariableTypeDescription
inputObjectFlow input data passed when starting the flow
context.variablesObjectFlow context variables set by previous steps

Security Behavior

In security contexts (permission evaluation), REL follows a fail-closed policy:

  • Parse errors evaluate to false (access denied)
  • Runtime evaluation errors evaluate to false (access denied)
  • This ensures that malformed or unexpected conditions never grant access

Grammar

expression     = or_expr
or_expr = and_expr ( "||" and_expr )*
and_expr = comparison ( "&&" comparison )*
comparison = additive ( ( "==" | "!=" | "<" | ">" | "<=" | ">=" ) additive
| "RELATES" additive "VIA" relation_types
[ "DEPTH" integer ".." integer ]
[ "DIRECTION" ( "OUTGOING" | "INCOMING" | "ANY" ) ] )?
additive = multiplicative ( ( "+" | "-" ) multiplicative )*
multiplicative = unary ( ( "*" | "/" | "%" ) unary )*
unary = "!" unary | "-" unary | postfix
postfix = atom ( "." identifier [ "(" args ")" ] | "[" expression "]" )*
atom = literal | identifier | "(" expression ")"
relation_types = STRING | "[" STRING ( "," STRING )* "]"
literal = null | boolean | integer | float | string | array | object
args = expression ( "," expression )*
array = "[" [ expression ( "," expression )* ] "]"
object = "{" [ identifier ":" expression ( "," identifier ":" expression )* ] "}"

Examples

// Simple comparisons
input.amount > 1000
input.status == 'active'

// Combined conditions
(input.priority >= 5 || input.urgent == true) && input.enabled == true

// String matching
input.category.contains('premium')
input.email.endsWith('@company.com')

// Array operations
input.tags.contains('vip')
input.items.length() > 0

// Path checks
node.path.descendantOf('/content/blog')
node.path.startsWith(auth.home)
node.path.depth() <= 3

// Role and group checks (in permission conditions)
auth.roles.contains('editor')
auth.groups.contains('admins')

// Ownership check
node.id == auth.local_user_id
node.path.startsWith(auth.home)

// Graph relationship (in permission conditions)
node.created_by RELATES auth.local_user_id VIA 'FRIENDS_WITH'
node.created_by RELATES auth.local_user_id VIA 'MANAGES' DEPTH 1..3 DIRECTION OUTGOING

// Arithmetic
input.price * input.quantity > 10000
(input.score + input.bonus) / input.attempts >= 80

// Method chaining
input.name.trim().toLowerCase().contains('admin')