Map Function
The map function evaluates JSONLogic expressions and writes the results to paths in the message context (or removes paths). It is the primary built-in function for data transformation.
{
"function": {
"name": "map",
"input": {
"mappings": [
{
"path": "data.full_name",
"logic": {"cat": [{"var": "data.first_name"}, " ", {"var": "data.last_name"}]}
}
]
}
}
}
Configuration
| Field | Type | Required | Description |
|---|---|---|---|
mappings | array | Yes | Ordered list of mappings to apply |
Mapping Fields
| Field | Type | Required | Description |
|---|---|---|---|
path | string | JSONLogic | Yes | Destination path in the context (data.*, metadata.*, or temp_data.*), or a JSONLogic expression that computes one; see Paths |
logic | JSONLogic | Required unless unset: true | Expression computing the value to write. By default, a null result is skipped; see Handling null Results |
unset | boolean | No | When true, removes the field at path instead of writing a value (cannot be combined with logic); see Removing a Path |
on_null | "skip" | "unset" | No | Behavior when logic evaluates to null: "skip" (default) leaves path unchanged; "unset" removes path |
mode | "set" | "append" | "extend" | No | Write mode: "set" (default) assigns the value at path; "append" and "extend" push onto an array at path; see Appending to an Array |
Mappings execute sequentially in the order they are listed, and each mapping immediately sees the changes made by earlier mappings in the same task:
{
"mappings": [
{"path": "temp_data.full_name", "logic": {"cat": [{"var": "data.first"}, " ", {"var": "data.last"}]}},
{"path": "data.greeting", "logic": {"cat": ["Hello, ", {"var": "temp_data.full_name"}]}}
]
}
Paths
Every destination path must start with one of the three context roots (data, metadata, or temp_data). Missing intermediate objects or arrays are created automatically:
{"path": "data.user.profile.name", "logic": "John"}
writes {"data": {"user": {"profile": {"name": "John"}}}}.
Object Keys vs. Array Indices
Path segments behave according to the container they traverse:
-
On an object: A segment is treated as an object key.
-
On an array (or missing container when the next segment is numeric): A numeric segment is treated as an array index (
0through1,048,575). Writing todata.list.2sets the third element ofdata.list, padding any missing earlier elements withnull:{"path": "data.list.0", "logic": "zero"}, {"path": "data.list.2", "logic": "two"}produces
data.list = ["zero", null, "two"]. -
Forcing an object key with
#: To write a numeric-looking object key instead of an array index, prefix the segment with#(one leading#is stripped):{"path": "data.items.#0", "logic": "first"}produces
{"items": {"0": "first"}}.
Numbers above 1,048,575 are always treated as object keys so a path can never allocate an unbounded array. Paths can have at most 64 segments.
Literal paths outside data, metadata, and temp_data are rejected at build time with INVALID_PATH.
Writing to a Context Root
Writing directly to a root ("path": "data", "metadata", or "temp_data") merges the returned object's keys into that root:
{"path": "data", "logic": {"merged": true}}
adds "merged": true to data while preserving existing keys in data. In the audit trail, new_value records the merged object ({"merged": true}) and old_value records the root prior to the merge.
Root paths only accept JSON objects: if logic is a non-object constant, compilation rejects the mapping at build time (INVALID_MAPPING); if a dynamic expression evaluates to a non-object at runtime, that mapping fails with MAPPING_ERROR and leaves the root unchanged.
Dynamic Destination Paths
Because path can be a JSONLogic expression, a single mapping can compute its destination path dynamically from the message:
{
"path": {"cat": ["data.accounts.", {"var": "data.id"}, ".balance"]},
"logic": {"var": "data.amount"}
}
When data.id is "ACC7" and data.amount is 42, this writes data.accounts.ACC7.balance = 42, and the audit entry records the resolved path ("data.accounts.ACC7.balance"). Static string paths are folded at build time with zero per-message cost.
Expressions (logic)
logic can be any literal JSON value or JSONLogic expression:
{"path": "data.copy", "logic": {"var": "data.original"}}
{"path": "data.status", "logic": "active"}
{
"path": "data.tier",
"logic": {"if": [
{">=": [{"var": "data.points"}, 1000]}, "gold",
{">=": [{"var": "data.points"}, 500]}, "silver",
"bronze"
]}
}
{
"path": "data.count",
"logic": {"reduce": [{"var": "data.items"}, {"+": [{"var": "accumulator"}, 1]}, 0]}
}
{
"path": "data.name",
"logic": {"if": [{"!!": {"var": "data.name"}}, {"var": "data.name"}, "Unknown"]}
}
Writing Literal Objects ($ Escape)
map evaluates expressions in templating mode: multi-key objects are treated as object templates whose values are evaluated recursively, while a single-key object whose key matches an enabled JSONLogic operator is evaluated as that operator. To write a literal object whose key collides with an operator name (or starts with $), prefix the key with $:
logic | Value Written |
|---|---|
{"cat": ["a", "b"]} | "ab" |
{"$cat": ["a", "b"]} | {"cat": ["a", "b"]} |
{"$$oid": "x"} | {"$oid": "x"} |
{"result": {"var": "data.n"}} | {"result": 600} (when data.n is 600) |
One leading $ is stripped from any template key, while single-key objects whose key is not an operator (like {"result": ...}) require no escape. See Literal keys and the $ escape.
Secrets Are Not Allowed in map
Because map writes its output into the Message (which is serialized and traced), neither logic nor path may reference {"secret": "name"}—the compiler rejects it at build time with SECRET_IN_MESSAGE_WRITE. To compute a value derived from a secret (such as an HMAC signature), use a custom host function. See Secrets.
Handling null Results
By default ("on_null": "skip"), if logic evaluates to null, the mapping is skipped: the target path retains its existing value (or remains absent) and no audit change is recorded:
{"path": "data.copy", "logic": {"var": "data.optional"}}
This default makes {"if": [cond, value, null]} naturally express "set when cond is true, otherwise leave unchanged." Note that a constant "logic": null with the default on_null is a no-op and is flagged by Compiler::check as a NULL_MAPPING advisory.
Removing a Path
To unconditionally delete a field or array element at path, set "unset": true (with no logic key):
{"path": "temp_data.retry", "unset": true}
To conditionally delete path whenever logic evaluates to null, keep logic and set "on_null": "unset":
{
"path": "temp_data.reason",
"logic": {"if": [{"var": "data.ok"}, null, "FAILED"]},
"on_null": "unset"
}
-
Removing a key that is already absent is a no-op and records no audit change.
-
Removing an existing key records a
Changewithremoved: true,old_valueset to the removed value, andnew_value: null:{"path": "data.y", "old_value": 2, "new_value": null, "removed": true} -
Removing an array index (such as
{"path": "data.gone.0", "unset": true}) removes that element and shifts subsequent elements down.
Why use
unsetinstead offalse? Setting a field tofalseleaves the key present in the object, somissingandexistsreport it as present,{"var": ["temp_data.x", "fallback"]}and??returnfalseinstead of"fallback", and the key stays in the serialized message. This matters especially in loops, wheretemp_datapersists across iterations—useunset: trueoron_null: "unset"to clear per-iteration state cleanly.
Build-Time Mapping Validation Rules
The compiler checks each mapping at build time and reports INVALID_MAPPING if any of these rules are violated:
| Rule | Reported Path | Explanation |
|---|---|---|
Must have logic or "unset": true | logic | Every mapping must either compute a value with logic or remove the path with "unset": true. |
Cannot set both logic and "unset": true | unset | unset: true takes no logic. To remove a path when logic is null, use "on_null": "unset" instead. |
on_null requires logic | on_null | on_null only applies when evaluating logic. |
mode requires logic | mode | mode only applies when writing a logic result. |
| Cannot unset a context root | path | Top-level roots (data, metadata, temp_data) cannot be removed; unset specific keys inside them instead. |
| Cannot append/extend a context root | path | Top-level roots are objects, not arrays; specify an array path under a root instead. |
| Root writes must be objects | logic | Writing to data, metadata, or temp_data requires a JSON object. |
Appending to an Array (mode: "append" and "extend")
-
"mode": "append"pushes the evaluated result as a single element onto the end of the array atpath:{ "path": "data.log", "logic": {"turn": {"var": "temp_data.turn"}, "move": {"var": "temp_data.move"}}, "mode": "append" } -
"mode": "extend"takes an array result fromlogicand pushes each of its elements onto the array atpath(for example,{"logic": [2, 3], "mode": "extend"}appends two elements rather than a nested array).
How append and extend handle the existing value at path:
- Missing or
null: Creates a new array atpathcontaining the appended element(s). - Existing array: Appends the element(s) in place at the end of the array.
- Any other type (object, string, number, boolean): Fails the mapping with
MAPPING_ERRORwithout modifyingpath. (Similarly,extendfails iflogicdoes not evaluate to an array ornull.)
Each appended element records a compact Change at its new index (data.log.0, data.log.1, etc.):
{"path": "data.log.0", "old_value": null, "new_value": {"turn": 1}},
{"path": "data.log.1", "old_value": null, "new_value": {"turn": 2}}
Performance tip: Always prefer
"mode": "append"over{"merge": [{"var": "data.log"}, [item]]}when accumulating items—especially inside a loop.mergeclones the entire array on every iteration ($O(n^2)$ over $n$ iterations), whereasappendmutates the array in place and records only the newly added element ($O(1)$ per append).
Handling Mapping Failures
A mapping fails at runtime if:
- Its
logicexpression fails to evaluate. - A dynamic
pathexpression fails to evaluate or resolves outsidedata,metadata, andtemp_data. - It attempts to unset, append to, or write a non-object to a context root.
- It uses
append/extendon a target path that holds a non-array value.
When a mapping fails:
- The engine records a
MAPPING_ERRORonmessage.errors()withpathset to the mapping's target path and a descriptive message naming the mapping index. - Any remaining mappings in the
maptask still execute. - The
maptask finishes with status500(which adds aTASK_STATUS_ERRORrecord and triggers the task'son_failurepolicy).
Note: If a mapping exceeds the configured operation budget (
ops_budget), the task aborts immediately withBUDGET_EXCEEDEDwithout running subsequent mappings.
Example of a partial mapping failure:
{
"id": "t",
"function": {
"name": "map",
"input": {
"mappings": [
{"path": "data.a", "logic": 1},
{"path": "data.s", "logic": "x", "mode": "append"},
{"path": "data.c", "logic": 3}
]
}
}
}
If data.s holds "str" (a string rather than an array), mapping 1 fails, while data.a = 1 and data.c = 3 are both written. The audit entry records status 500 with those two changes, message.errors() receives MAPPING_ERROR (for mappings[1]) and TASK_STATUS_ERROR (for task t), and the task's on_failure policy decides whether the workflow continues or stops.
Audit Trail
When RunOptions::capture_changes is enabled (the default), every write or removal made by map adds a Change to the task's AuditEntry containing path, old_value, new_value, and removed. See Audit Trails.
Try It
Open the Full Debugger UI for step-by-step execution and workflow visualization.
Best Practices
- Use
temp_datafor Intermediate Values — Keep temporary calculations intemp_dataand write final outputs todata. - Order Mappings Intentionally — Later mappings in the same task can read fields written by earlier mappings.
- Use
unsetto Delete Keys — Remember thatnullresults are skipped by default (on_null: "skip") andfalsekeeps the key present; use"unset": trueor"on_null": "unset"to remove a field. - Use
"mode": "append"for Growing Arrays — Avoid cloning arrays withmergeinside loops.