Array Processing¶
The rule engine provides powerful array processing capabilities for handling batch messages. This is essential when third-party systems send multiple events in a single message, or when you need to check if any element in an array matches specific criteria.
Array Operators in Conditions¶
Use array operators to check if a message is relevant by inspecting array contents.
Available Operators:
- any: At least one array element matches nested conditions
- all: All array elements match nested conditions
- none: No array elements match nested conditions
Example: Check if any notification is critical
conditions:
operator: and
items:
- field: "{type}" # ✅ NEW: Explicit template syntax
operator: eq
value: "BATCH_NOTIFICATION"
# Check if ANY notification in the array is critical
- field: "{notifications}" # ✅ NEW: Explicit template syntax
operator: any
conditions:
operator: and # Required for nested conditions
items:
- field: "{severity}" # ✅ NEW: Explicit template syntax
operator: eq
value: "critical"
How it works:
- Iterates through the notifications array
- Evaluates nested conditions against each element
- Short-circuits on first match for any (performance optimization)
- Returns true if the operator condition is satisfied
ForEach Actions¶
Generate one action per array element using forEach. This is the key feature for batch processing.
NEW: Consistent Brace Syntax¶
All variable references now use {braces} for consistency:
- Condition fields: {field} ✅
- Condition values: {field} or literal ✅
- ForEach fields: {arrayField} ✅ NEW!
- Action subjects: {field} ✅
- Action payloads: {field} ✅
Basic Syntax:
action:
nats:
forEach: "{notifications}" # ✅ NEW: Braces required!
subject: "alerts.{id}"
payload: '{"id": "{id}", "message": "{message}"}'
With Filter (recommended):
action:
nats:
forEach: "{notifications}" # ✅ NEW: Braces required!
filter: # Only process elements matching these conditions
operator: and # Required
items:
- field: "{severity}" # ✅ Explicit template syntax
operator: eq
value: "critical"
subject: "alerts.critical.{id}"
payload: |
{
"id": "{id}",
"message": "{message}",
"severity": "{severity}"
}
What happens:
1. Extracts the notifications array from the message
2. Applies filter conditions to each element (if specified)
3. For each matching element, generates one action
4. Templates subject/payload using fields from that element
Template Context: The @msg Prefix¶
When using forEach, template variables can refer to either:
- Array element fields: Use {fieldName} directly
- Root message fields: Use {@msg.fieldName} explicitly
| Context | {field} resolves to |
{@msg.field} resolves to |
|---|---|---|
| Normal action (no forEach) | Root message field | Root message field (explicit) |
| ForEach action | Current array element field | Root message field (explicit) |
Example:
action:
nats:
forEach: "{alerts}" # ✅ NEW: Braces required!
subject: "alerts.{alertId}"
payload: |
{
"alertId": "{alertId}", # From alerts[i]
"severity": "{severity}", # From alerts[i]
"deviceId": "{@msg.deviceId}", # From root message
"timestamp": "{@msg.receivedAt}", # From root message
"processedAt": "{@timestamp()}" # System function
}
ForEach with Variable Comparisons (NEW!)¶
You can now use variable comparisons in forEach filters:
# Message: {"min_value": 50, "readings": [{"value": 30}, {"value": 75}, {"value": 90}]}
action:
nats:
forEach: "{readings}" # ✅ Braces required!
filter:
operator: and
items:
- field: "{value}" # ✅ Element field
operator: gt
value: "{@msg.min_value}" # ✅ Compare to root message field!
subject: "sensors.high-reading.{@uuid7()}"
payload: |
{
"value": {value},
"threshold": "{@msg.min_value}"
}
With KV Lookups:
# KV: thresholds["sensor-batch"] = {"min": 10, "max": 100}
action:
nats:
forEach: "{readings}" # ✅ Braces required!
filter:
operator: and
items:
- field: "{value}" # ✅ Element field
operator: gte
value: "{@kv.thresholds.{@msg.sensor_type}:min}" # ✅ Dynamic KV threshold!
- field: "{value}"
operator: lte
value: "{@kv.thresholds.{@msg.sensor_type}:max}"
subject: "sensors.valid-reading.{@uuid7()}"
payload: '...'
Complete Example: Batch Notification Processing¶
Scenario: Security system sends batch motion alerts. Generate one alert per camera.
Rule:
- trigger:
nats:
subject: security.notifications
conditions:
operator: and
items:
# Check message type
- field: "{type}" # ✅ Explicit template syntax
operator: eq
value: "MOTION_BATCH"
# Check if ANY alert is from a camera we care about
- field: "{alerts}" # ✅ Explicit template syntax
operator: any
conditions:
operator: and
items:
- field: "{deviceType}" # ✅ Explicit template syntax
operator: eq
value: "camera"
action:
nats:
# Generate one alert per camera motion event
forEach: "{alerts}" # ✅ NEW: Braces required!
filter:
operator: and
items:
- field: "{deviceType}" # ✅ Explicit template syntax
operator: eq
value: "camera"
- field: "{motionDetected}" # ✅ Explicit template syntax
operator: eq
value: true
subject: "alerts.motion.{buildingId}.{cameraId}"
payload: |
{
"cameraId": "{cameraId}",
"location": "{location}",
"motionDetected": true,
"timestamp": "{timestamp}",
"buildingId": "{@msg.buildingId}",
"batchId": "{@msg.batchId}",
"processedAt": "{@timestamp()}"
}
headers:
X-Alert-Type: "motion"
X-Building-Id: "{@msg.buildingId}"
Primitive Array Elements¶
ForEach works with primitive arrays (strings, numbers) using the {@value} accessor:
String Array:
# Message: {"action": "provision", "device_ids": ["device-001", "device-002"]}
action:
nats:
forEach: "{device_ids}" # ✅ Braces required!
subject: "devices.provision.{@value}" # Access primitive with @value
payload: |
{
"device_id": "{@value}",
"action": "{@msg.action}",
"timestamp": "{@timestamp()}"
}
Number Array with Filter:
# Message: {"sensor_id": "temp-001", "readings": [23.5, 24.1, 25.3, 26.0]}
action:
nats:
forEach: "{readings}" # ✅ Braces required!
filter:
operator: and
items:
- field: "{@value}" # Access primitive with @value
operator: gt
value: 25
subject: "sensors.high-reading.{@msg.sensor_id}"
payload: |
{
"sensor_id": "{@msg.sensor_id}",
"reading": {@value},
"timestamp": "{@timestamp()}"
}
Nested Array Paths¶
ForEach supports deeply nested array paths:
# Message: {"data": {"sensors": {"readings": [{"value": 10}]}}}
action:
nats:
forEach: "{data.sensors.readings}" # ✅ Nested path with braces
subject: "sensors.reading.{@uuid7()}"
payload: '{"value": {value}}'
Root Array (@items)¶
When the entire message is an array:
# Message: [{"device": "dev1"}, {"device": "dev2"}]
action:
nats:
forEach: "{@items}" # ✅ Root array with braces
subject: "devices.{device}.status"
payload: '{"device": "{device}"}'
Performance & Limits¶
Default Limits:
- Maximum 100 iterations per forEach (configurable)
- Prevents resource exhaustion from malicious/malformed messages
- Configure via forEach.maxIterations in config file
Configuration:
Best Practices¶
✅ DO:
- Use braces for all variable references: {field}, {@items}, {@msg.field}
- Use filter to limit iterations
- Use array operators in conditions to pre-filter messages
- Use {@msg} prefix explicitly when accessing root message fields
- Test with empty arrays and non-matching elements
❌ DON'T:
- Forget braces on forEach: forEach: "field" ❌ Should be forEach: "{field}" ✅
- Process unbounded arrays without limits
- Duplicate logic between array operators and forEach filters
- Assume all array elements are objects (primitives need {@value})
- Forget that {field} resolves to array element in forEach context