blue-bex-java is a compiled Java engine for Blue Expression Objects
(BEX): a deterministic scripting model written as Blue data.
BEX is for logic that should live inside Blue documents instead of host code. A BEX program can read a Blue document view, host-provided bindings, constants, variables, and prior results, then compute structured Blue-compatible data. Because BEX programs are Blue data, they can participate in the same content identity and BlueId model as the documents that carry them.
BEX is not JavaScript, WASM, a network runtime, an LLM extension point, or a
contract processor. It does not mutate documents or perform external actions.
It computes a deterministic BexExecutionResult; the host decides how to use
that result.
BEX programs are Blue object trees. An object with exactly one key beginning
with $ is an operator. Objects with normal keys are literal Blue-compatible
objects. Lists and scalar values are literal values unless nested inside an
operator body.
BEX is useful when a host wants deterministic, document-owned logic for policy checks, projections, validation rules, event payload construction, patch construction, fixture generation, or other Blue-native computation.
BEX keeps logic close to the data it describes. That matters when logic should be versioned, hashed, inspected, signed, transported, or reproduced as Blue data instead of hidden in host application code.
The runtime is intentionally small: no time, random, network, filesystem,
JavaScript, or WASM in the core language. Host capabilities are available only
through explicitly registered $intrinsic processors keyed by BlueId.
do:
- $let:
name: status
expr:
$document: /status
- $let:
name: limit
expr:
$integer:
$binding:
name: policy
path: /maxAmount
- $return:
approved:
$and:
- $eq:
- $var: status
- active
- $gte:
- $var: limit
- 1000
message:
$concat:
- "Status is "
- $var: statusWith /status = active and a policy binding containing
/maxAmount = 1000, the result value is:
approved: true
message: Status is activeFrozenNode programNode = FrozenNode.fromResolvedNode(program);
FrozenNode documentNode = FrozenNode.fromResolvedNode(document);
Map<String, Object> policyMap = new LinkedHashMap<>();
policyMap.put("maxAmount", 1000);
BexExecutionContext context = BexExecutionContext.builder()
.document(new FrozenBexDocumentView(documentNode))
.binding("policy", BexValues.fromSimple(policyMap))
.binding("event", BexValues.nodeSnapshot(eventNode))
.gasLimit(100_000)
.build();
BexExecutionResult result = BexEngine.builder()
.build()
.compileAndExecute(BexProgramSource.inline(programNode), context);Programs that use shared functions/constants can use a definition node:
BexProgramSource source = BexProgramSource.withDefinition(
programNode,
definitionNode,
"entryFunction"
);Programs that use host capabilities register intrinsic processors on the engine. The BEX program names the operation with a normal Blue type/BlueId; the processor registry decides whether that operation is supported:
BexEngine engine = BexEngine.builder()
.intrinsic(CommonCryptoEd25519Verify.class, invocation -> {
invocation.chargeGas(500);
// Read invocation.field("publicKey"), invocation.field("message"),
// and invocation.field("signature"), then return a BexValue boolean.
return BexValues.scalar(verifySignature(invocation));
})
.build();$const must reference a declared program constant. Unknown constants fail at
compile time instead of evaluating to undefined:
constants:
amount: 400
expr:
$const: amountBEX function arguments may declare Blue type or shape patterns. BEX does not have a separate type enum or type system; declared argument patterns are Blue nodes and runtime values are checked with Blue's node/type matcher.
functions:
capture:
args:
amount:
type: Integer
hotelOrder:
blueId: HotelOrderBlueId
request:
customerName:
type: Text
schema:
required: true
nights:
type: Integer
schema:
required: true
expr:
amount:
$var: amount
order:
$var: hotelOrderAll declared function arguments are required by the function call ABI for now.
Unknown functions, extra argument names, and missing declared arguments fail at
compile time. Typed arguments are checked after their call expressions are
evaluated; a runtime mismatch throws BexException.
Text "400" does not match the Blue Integer pattern. Use an explicit
conversion when conversion is intended:
$call:
function: capture
args:
amount:
$integer:
$event: /message/request/amountUntyped required arguments remain supported by declaring an empty pattern:
args:
input: {}When BEX converts computed values back to Blue nodes for $is, function
argument checks, or output conversion, Blue language keys keep their Blue
meaning. For example, this computed value is a node with a type field and a
status property, not an object with an ordinary property named type:
type:
blueId: HotelOrderType
status: confirmedA bare blueId object is a Blue reference pattern:
blueId: HotelOrderTypeDo not combine blueId with sibling fields to describe a typed instance. Use
type: { blueId: ... } for typed values.
BEX programs are Blue documents, so BEX syntax must use valid Blue authoring
forms. For user-defined name containers such as functions, constants,
args, and $call.args, do not use Blue language keys or invalid reserved keys
as names. This includes value, items, blueId, type, schema, name,
description,
itemType, keyType, valueType, mergePolicy, constraints, contracts,
properties, $previous, and $pos.
For operator bodies, payload/reference/control keys such as value, items,
blueId, contracts, properties, $previous, and $pos cannot be used as ordinary
multi-field operands. Use BEX operand names such as node, list, input,
pattern, object, key, path, val, cond, then, and else.
Metadata keys such as name, description, type, schema, itemType,
keyType, valueType, and contracts are legal Blue language fields, but they are not
ordinary object properties. constraints is invalid in Blue Language 1.0. An
operator may use one of the legal metadata fields only when the BEX compiler
explicitly supports that field.
Function argument patterns and $is.pattern are static Blue patterns. BEX does
not evaluate expressions inside those patterns, and it does not emulate Blue
authoring sugar such as inline type: Integer preprocessing for computed type
fields.
BexDocumentView owns document pointer resolution, canonical reads, resolved
reads, and the current scope path.
FrozenBexDocumentView view = new FrozenBexDocumentView(
canonicalRoot,
resolvedRoot,
"/orders/123"
);$document reads the canonical view by default:
$document: /statusResolved view reads are explicit:
$document:
path: /status
view: resolvedHosts can provide arbitrary named bindings:
BexExecutionContext.builder()
.document(view)
.binding("policy", policyValue)
.binding("actor", actorValue)
.binding("event", eventValue)
.build();Use $binding for arbitrary host bindings:
$binding:
name: policy
path: /maxAmountShort form is supported:
$binding: actor/nameHosts often provide common bindings such as event, steps, and
currentContract. BEX includes shortcuts for those common names:
$event;$steps;$currentContract.
For every other host binding, use $binding.
BEX has two pointer contexts.
Document pointers are resolved relative to the current document scope:
$document;$resultValue;$appendChange.path;$appendChangesentrypath.
Value pointers are resolved inside the selected value and are not affected by the document scope:
$event;$currentContract;$steps.path;$binding.path;$pointerGet.path;$pointerSet.path.
Static omitted/default paths may intentionally read a root/default location.
Dynamic pointer expressions that evaluate to null or undefined fail instead
of silently becoming the current document scope or root value.
Dynamic text operands also reject null and undefined. This applies to
operator fields such as $get.key, $objectSet.key, $hasKey.key,
$binding.name, $steps.step, $appendChange.op, and $pointerSet.op. A
static empty string is still allowed when it is explicitly authored.
Use $pointerJoin when building document paths from dynamic path segments. Each
item is treated as one JSON Pointer segment and escaped safely:
$pointerJoin:
- orders
- $var: orderId
- statusIf orderId is abc/def~ghi, the result is
/orders/abc~1def~0ghi/status.
BEX operators are Blue objects whose single key starts with $.
| Operator | Purpose |
|---|---|
$document |
Read from the Blue document view. |
$binding |
Read a host-provided runtime binding. |
$event |
Shortcut for the common event binding. |
$steps |
Shortcut for prior step/result bindings when a host provides them. |
$currentContract |
Shortcut for a host-provided current contract binding. |
$var |
Read a local variable. |
$const |
Read a program constant. |
$get |
Read an object field by key. |
$var and $const support object-form value paths. The name is static, so
variables and constants still compile to fixed slots/lookups, while path is a
value-local JSON Pointer inside the selected value:
$var:
name: request
path: /summary$const:
name: Policy/minimum
path: /amountThe path may be static text or an expression that evaluates to pointer text.
Missing path targets return undefined, matching $pointerGet/value reads.
There is intentionally no $var: request/summary shorthand; existing variable
and constant names may contain /. Dynamic variable or constant names are not
supported; use $get or $pointerGet for dynamic object lookup.
| Operator | Purpose |
|---|---|
$unwrap |
Unwrap Blue scalar wrapper values. |
$is |
Return whether a value matches a Blue pattern. |
$kind, $isKind |
Inspect the visible BEX runtime value kind. |
$text |
Convert to text. |
$integer |
Convert to exact integer. |
$number |
Convert to exact decimal/number. |
$boolean |
Convert to boolean. |
$object |
Require an object, or default undefined to an empty object. |
$list |
Require a list, or default undefined to an empty list. |
$is.pattern is static Blue pattern data, not a BEX expression:
$is:
node:
$event: /message/request/amount
pattern:
type: Integer$kind returns one of undefined, null, text, integer, double,
boolean, object, or list. This is BEX runtime shape, not Blue type
conformance; use Blue schema/type validation or $is for Blue type semantics.
$isKind:
val:
$event: /message/request/amount
kind: [integer, double]$isKind.kind may be a single kind or a list of kinds.
| Operator | Purpose |
|---|---|
$concat |
Concatenate text. |
$pointerJoin |
Safely build a JSON Pointer from path segments. |
$join |
Join list items with a separator. |
$split |
Split text. |
$startsWith |
Check a prefix. |
$sliceAfter |
Return text after a prefix. |
$join:
list:
- a
- b
separator: ":"| Operator | Purpose |
|---|---|
$eq, $ne |
Equality and inequality. |
$gt, $gte, $lt, $lte |
Numeric comparisons. |
$and, $or, $not |
Boolean logic with short-circuiting. |
$truthy, $empty, $isEmpty |
Truthiness checks. $isEmpty is an alias that avoids Blue list-placeholder syntax in list operands. |
$exists |
Return false only for undefined values. |
$coalesce, $default |
First non-empty value. $default is an alias. |
$exists is useful for optional-field validation because it distinguishes a
missing value from present falsy values. It returns true for null, false,
0, empty text, and empty list/object values:
$or:
- $not:
$exists:
$event: /message/request/note
- $is:
node:
$event: /message/request/note
pattern:
type: Text| Operator | Purpose |
|---|---|
$add |
Add exact integers. |
$subtract |
Subtract exact integers. |
$multiply |
Multiply exact integers. |
$divide |
Divide exact integers; non-exact division fails. |
| Operator | Purpose |
|---|---|
$keys |
Sorted object keys. |
$entries |
Sorted object entries as { key, val }. |
$size |
Size of a list, object, or scalar value. |
$listGet |
Read a list item. |
$listConcat |
Concatenate lists. |
$merge |
Shallow object merge. |
$objectSet |
Set a dynamic object key without mutating the input. |
$pointerGet |
Read by JSON Pointer from any value. |
$pointerSet |
Return a value with a JSON Pointer update. |
$map, $filter, $flatMap, $reduce |
Deterministic collection projection, selection, fanout, and aggregation. |
$some, $find, $findEntry |
Short-circuit collection queries. |
$includes |
List membership using BEX equality. |
$hasKey |
Object key membership. |
$objectFromEntries |
Build an object from { key, val } entries. |
Collection expressions accept lists or objects. Lists iterate in item order.
Objects iterate in sorted key order, matching $keys, $entries, and
$forEach object behavior. item, optional key, and optional index
bindings are local to the query expression and are restored afterwards, so they
do not overwrite outer variables with the same names.
$map:
in:
$event: /message/request/changeset
item: patch
expr:
op:
$var:
name: patch
path: /op
path:
$var:
name: patch
path: /pathCollection operator shapes and results:
| Operator | Shape | Result |
|---|---|---|
$map |
in, item, optional key/index, expr |
List of expr results. Object input still returns a list in sorted-key order. |
$filter |
in, item, optional key/index, where |
Filtered list for list input; filtered object for object input. |
$flatMap |
in, item, optional key/index, expr |
Concatenated list. Each expr result must be a list. |
$reduce |
in, acc, init, item, optional key/index, expr |
Final accumulator. init is evaluated once before iteration. |
$some |
in, item, optional key/index, where |
Boolean, short-circuiting at the first truthy where. |
$find |
in, item, optional key/index, where |
First matching item/value, or undefined. |
$findEntry |
in, item, optional key/index, where |
First matching entry object, or undefined. |
For list input, $findEntry returns:
val: <item>
index: <zero-based-index>For object input, $findEntry returns:
key: <object-key>
val: <field-value>
index: <zero-based-sorted-key-index>$includes has shape { list, val }, requires list to evaluate to a list,
uses BEX equality, and short-circuits on the first equal item. $hasKey has
shape { object, key }; it returns false for non-object inputs and true when
the object has a non-undefined value for the key.
$objectFromEntries expects a list of objects with key and val fields:
$objectFromEntries:
$map:
in:
$entries:
b: 2
a: 1
item: entry
expr:
key:
$var:
name: entry
path: /key
val:
$multiply:
- $var:
name: entry
path: /val
- 10Entry keys cannot be undefined or null; those fail. Entry values may be
undefined, which omits/removes that key from the result. Duplicate keys use
the last non-undefined value, unless a later undefined value removes the key.
| Operator | Purpose |
|---|---|
$changeset |
Return accumulated patch entries. |
$events |
Return accumulated event/data entries. |
$resultValue |
Read the document value implied by accumulated changes. |
| Operator | Purpose |
|---|---|
$choose |
Conditional expression. |
$call |
Call a local function. |
$intrinsic |
Invoke a registered host intrinsic by the BlueId of its static type. |
$literal |
Return payload without compiling nested operators. |
$null, $emptyObject, $emptyList |
Emit explicit null, empty object, or empty list values after Blue source normalization. |
$literal prevents normal expression compilation, but BEX still rejects
BEX-looking operators inside Blue type-definition fields such as type,
itemType, keyType, valueType, blue, and schema.
$intrinsic is the only host-extension expression. It does not require a
special Blue/BEX Intrinsic supertype. The type may be any static Blue type
or Blue value whose BlueId can be resolved. BEX takes that BlueId and looks up
a registered processor.
$intrinsic:
type:
blueId: CTkdsd4MNjiFA13MFeAx34jnnBLfGzn7HfP6fx1dV43s
publicKey:
$const: trustedSignerPublicKey
message:
$event: /message/canonicalBytes
signature:
$event: /message/signatureRules:
typeis static authored Blue data. BEX expressions insidetypeare rejected, because the compiler must know the intrinsic BlueId before execution.- Hosts can register processors directly by BlueId or by a Java class with a
resolvable
@TypeBlueId. - The payload is the normal fields of the typed operation object. There is no
argsorparamswrapper. - The
typefield itself is not passed as a payload field. Processors receivetypeseparately asinvocation.type(). - Payload field expressions are evaluated normally. Fields that evaluate to
undefinedare omitted, matching object literal behavior. - Compilation fails if the active engine has no intrinsic processor registered for the resolved BlueId. Execution checks the same support boundary again so a shared compiled-program cache cannot bypass it.
- The processor returns a
BexValue. AnullJava return is normalized to BEXundefined. - The processor is responsible for its own gas accounting by calling
invocation.chargeGas(...).
The Blue type definition and its description/spec text define what the operation means. For standard intrinsics, keep conformance vectors beside the spec text so independent processors can prove they implemented the same behavior. For Ed25519, the definition must be explicit about what bytes are signed; do not describe verification over a generic object without also naming the canonical byte representation.
| Statement | Purpose |
|---|---|
$let |
Define or initialize a local variable. |
$set |
Update an existing local variable. |
$if |
Conditional branch. |
$forEach |
Iterate list items or object entries. |
$appendChange |
Append a patch entry to the result changeset. |
$appendChanges |
Append many patch entries. |
$appendEvent |
Append an event/data value. |
$appendEvents |
Append many event/data values. |
$call |
Call a local function for side effects and return handling. |
$return |
Return the result value. |
$returnIf |
Return early when a condition is truthy. |
$fail |
Fail deterministically. |
$failIf |
Fail deterministically when a condition is truthy. |
$returnIf returns from the current function/root when cond is truthy:
$returnIf:
cond:
$empty:
$event: /message/request/summary
expr:
changeset: []
events:
- type: Conversation/Proposed Change Invalid
reason: summary is missingexpr is optional. When omitted, $returnIf returns the default result value,
the same as bare $return. The payload field is named expr; value is not
accepted because value is a Blue scalar-wrapper field and cannot safely carry
an object payload in authored Blue YAML.
$failIf fails deterministically when cond is truthy:
$failIf:
cond:
$not:
$exists:
$event: /message/request/id
message: request id is requiredThe expr and $failIf.message operands are lazy; they are evaluated only
when the guard condition is truthy.
$let also supports a multi-bind form. Without order, the bindings are
parallel: all expressions read the frame as it existed before the $let, then
all variables are assigned. Unordered bindings are sorted only to make execution
deterministic; they do not create dependencies by name.
With order, bindings are sequential and later bindings may read earlier ones.
order must list every key in vars exactly once:
$let:
order: [request, summary]
vars:
request:
$event: /message/request
summary:
$var:
name: request
path: /summaryIn unordered form, a binding cannot read another new binding from the same
vars block unless that variable already existed before the block. Use order
when one binding depends on another.
$forEach can bind list indexes and object keys when those are needed for
patch paths:
$forEach:
in:
$event: /message/request/orders
item: order
index: i
do:
- $appendChange:
op: replace
path:
$pointerJoin:
- orders
- $var: i
- status
val: receivedFor object iteration, use key and item to bind the object key and value
separately. The older form with only item still binds { key, val }.
BexExecutionResult contains:
value, the primary return value;changeset, the standard patch accumulator;events, the standard event/data accumulator;gasUsed;metrics.
BEX computes these values only. The host decides whether patches are applied, events are emitted, or accumulators are treated as ordinary data.
$resultValue reads the document value after applying accumulated patches in
order. Parent reads reflect descendant object patches, so reading
/hotelOrder after replacing /hotelOrder/status returns the original
hotelOrder object with the updated status. Current materialization supports
object paths, list index replacement, and non-shifting list index removal.
Removing a list index creates a sparse overlay slot: the removed index reads as
undefined, later indexes keep their positions, and converting the whole sparse
overlay list to Blue output fails under the strict host-boundary profile.
$appendChanges validates each patch entry the same way as $appendChange.
Supported patch operations are add, replace, and remove. add and
replace require a non-undefined val; remove does not include a value.
$appendEvents validates each item the same way as $appendEvent. Undefined
event values are rejected. BEX core does not require events to be objects; hosts
decide what event shape they accept.
BEX execution is deterministic for a fixed program, context, document view, bindings, gas schedule, and immutable host boundary values.
Use BexValues.nodeSnapshot(node) for untrusted mutable Node values. Use
BexValues.nodeCursorTrustedImmutable(node) only when the host can guarantee
the node will not be mutated during execution.
The engine is compiled-first:
- selected programs compile lazily;
- compiled programs are cacheable by stable Blue node identity and entry name;
- variables use slot frames;
- static pointers are parsed at compile time;
- document and binding reads use cursor-backed values where possible;
$objectSetand$pointerSetuse overlay values;$resultValuematerializes accumulated patch overlays for reads;- output conversion to
Node,FrozenNode, or simple Java values is explicit.
Every result includes BexMetrics:
BexMetrics metrics = result.metrics();
metrics.compiledExecutions();
metrics.compileCacheHits();
metrics.frozenDocumentReads();
metrics.nodeMaterializations();The deterministic gas rules are specified in docs/GAS.md. The
portable fixture format is documented in docs/FIXTURES.md.
The rich fixture suite lives under src/test/resources/rich-fixtures/ and
currently has 159 cases: 53 current behavior fixtures, 3 parse-error fixtures,
and 103 gas fixtures. The local fixture package is kept aligned with the
canonical BEX spec fixtures under blue-spec/specifications/bex/1.0/fixtures/.
The translated corpus strategy is documented in docs/TRANSLATED_CORPUS.md. It contains 80 representative Kyverno, JMESPath/JSONata, and JSON Patch-style cases translated into BEX to test whether the small query/operator core is sufficient for common policy, transform, and patch-emission workflows: 30 Kyverno validate-style cases, 20 Kyverno mutate/generate-style cases, 20 JMESPath/JSONata-style query/transform cases, and 10 JSON Patch emission edge cases.
./gradlew test --tests '*BexRichFixtureTest'
./gradlew test --tests '*BexTranslatedCorpusTest'
./gradlew test
./gradlew buildMIT. See LICENSE.