Beta: in progress. These pages describe what the next release is getting, from releases/r10.0-develop, and may change before it ships. For the version you can run today, read the stable documentation.
Build integrations · Beta
Mapping
The rules-based mapper and the legacy Scriban JSON mapper
A mapper turns what arrived into what the destination expects. Bitween has two built-in mappers, each with its own visual editor.
| Rules-based mapper | Legacy JSON mapper | |
|---|---|---|
| Id | NativeMapper |
NativeJSONMapper |
| Formats | JSON and XML, in either direction | JSON to JSON |
| Stored as | Mapping rules in JSON | A Scriban template |
| Partner and global values | Passed as separate context | Injected into the payload as __partner__ and __globals__ |
| Status | Current | Retiring |
Open either editor from a subscription's Transformation stage with Open the visual mapping editor. Its address is /subscriptions/{id}/mapper.
Rules-based mapper#
How it runs#
- The source format reads the payload into a tree.
- The rules build a new tree.
- The target format writes the tree, with content type
application/jsonorapplication/xml.
Every rule is attempted. If any rule fails, the exchange fails with a message listing each failing target and the reason, instead of producing a document with gaps.
The rules are read once per rule set and kept, and the source document is read from the parse the exchange already made of it when there is one. Neither changes what a mapping produces: numbers are still read exactly and dates still as text. See SharedPayloadParse and CacheCompiledTemplates in Configuration.
Stored properties#
| Property | Content |
|---|---|
MappingRules |
The rules as JSON. Required at run time. |
SourceSample, TargetSample |
Samples kept for the editor. Never read while mapping. |
Rules format#
{
"version": 1,
"sourceFormat": "json",
"targetFormat": "json",
"sourceDateOrder": "dayFirst",
"fields": [
{ "target": ["reference"], "from": { "kind": "path", "path": "order.id" } },
{ "target": ["customer", "name"], "from": { "kind": "path", "path": "order.customer" },
"transform": { "fn": "upper" } },
{ "target": ["account"], "from": { "kind": "partner", "key": "accountNumber" } },
{ "target": ["region"], "from": { "kind": "global", "setId": "regions", "key": "default" } },
{ "target": ["status"], "from": { "kind": "path", "path": "order.status" },
"lookup": { "table": { "N": "New", "S": "Shipped" }, "fallback": "Unknown" } },
{ "target": ["orderedOn"], "from": { "kind": "path", "path": "order.date" },
"transform": { "fn": "formatDate", "format": "yyyy-MM-dd" } }
],
"lists": [
{
"over": "order.lines",
"target": ["items"],
"where": { "field": "qty", "operator": "greaterThan", "value": 0 },
"fields": [
{ "target": ["sku"], "from": { "kind": "path", "path": "sku" } },
{ "target": ["quantity"], "from": { "kind": "path", "path": "qty" }, "type": "number" },
{ "target": ["orderRef"], "from": { "kind": "rootPath", "path": "order.id" } }
]
}
]
}
Rules with a version above 1 are refused.
Field rules#
A field rule writes one value.
target is a list of path segments, so a key containing a dot can still be written. Missing objects along the path are created.
from says where the value comes from.
kind |
Value |
|---|---|
path |
A dot path read from the current scope, which is the document or the current list item |
rootPath |
A dot path read from the document root, even inside a list |
fixed |
The literal in value, keeping its JSON type |
partner |
The partner property named key |
global |
The value key in the global value set setId |
source |
A path into the original document: the input of the exchange whose delivery got this response. Read like any path. Only a response or bus gateway subscription has these; the editor calls them Original. |
A missing path, partner property, global value or source value gives null rather than an error. Paths match keys exactly and never step into a list. Use a list rule to reach list items.
The value then passes through these optional steps, in order.
transformapplies one function.lookupreplaces the value fromtable, comparing as text. A value with no entry becomesfallback, which defaults to null.typeconverts the value tostring,numberorboolean.
Transforms#
fn |
Arguments | Result |
|---|---|---|
upper, lower, trim |
||
substring |
start, optional length |
|
replace |
find, with |
|
concat |
with |
Appends text |
round |
decimals, 0 to 15 |
Rounds halves away from zero |
multiply |
by |
|
add |
amount |
|
formatDate |
format |
Parses the value as a date and formats it |
defaultIfEmpty |
value |
Replaces null or empty text |
Null passes through every transform except defaultIfEmpty.
Numbers, booleans and dates#
- Numbers are decimals throughout, so
100 × 1.16is exactly116. Whole numbers are written without a decimal point, and scientific notation is never used. - Conversion to boolean accepts
true,1,yes,y,false,0,no,nand the numbers 0 and 1. Anything else fails the rule. formatDatealways accepts ISO-style dates such as2026-09-10,2026-09-10T08:30:00Zand20260910. Day-first dates such as10/09/2026are accepted only whensourceDateOrderisdayFirst, and month-first dates only when it ismonthFirst. Ambiguous dates are refused, never guessed.
List rules#
A list rule writes an array.
| Property | Meaning |
|---|---|
over |
What to walk. A path walks that list. An empty string walks the document itself. Leaving it out walks nothing, so the list holds only its fixed entries. |
target |
Where the array is written. |
where |
Optional filter with field, operator and value. equal and notEqual compare as text. greaterThan, greaterThanOrEqual, lessThan and lessThanOrEqual compare as numbers. Items without the field are skipped. |
fields, lists |
Rules applied to each item, with paths relative to the item. Lists can nest. |
item |
One field rule that makes each entry a plain value rather than an object. |
fixed |
Entries placed before the walked items, each with its own item, fields or lists, read from the enclosing scope. |
A top-level root list rule makes the whole output an array. It cannot be combined with top-level fields or lists.
XML#
When reading XML:
- The root element's name becomes the top-level key.
- Attributes become
@namekeys, and mixed text becomes#text. - Repeated elements become lists. A list path that finds a single element walks it as a list of one.
- Namespaces are dropped and only local names are kept.
- DTDs and nesting deeper than 64 levels are refused.
When writing XML:
- The output needs exactly one top-level key, and it cannot be a list.
- A list directly inside another list is refused.
- Fields named
@xmlnsor@xmlns:prefixbecome namespace declarations. A prefix without a declaration is refused.
CSV#
sourceFormat and targetFormat can be csv, for delimited text, with sourceCsv and targetCsv saying
how each side is delimited, since a partner's semicolon file often becomes someone else's comma file:
{ "version": 1, "sourceFormat": "csv", "targetFormat": "json",
"sourceCsv": { "delimiter": ";", "hasHeader": true },
"root": { "over": "", "fields": [ { "target": ["sku"], "from": { "kind": "path", "path": "SKU" } } ] } }
| Option | Default | |
|---|---|---|
delimiter |
, |
Any characters: ;, |, \t for tab |
hasHeader |
true |
Whether the first line names the columns. Without a header, columns are named by position: 1, 2, 3. |
byteOrderMark |
false |
When writing, start the file with a UTF-8 byte-order mark, so Excel opens accented names correctly |
When reading CSV, the document is a list of rows, so a list rule walks it with "over": "", and root
makes the whole output a list, one entry per row. Every field
is text: nothing turns 041800 into a number, and a rule's own type does when one is wanted. A row
wider than the header gets its extra fields named by position rather than dropped. Blank rows are skipped,
and a file that is only a header is a list of no rows.
When writing CSV, the output is a list of rows, or one object for a single row. Every column any row has is
written, in the order they first appear, and a row without one gets an empty field there. A nested field
becomes a dotted column name, as destination.city. A list inside a row is refused rather than folded into
one cell.
The information type of a CSV document is carried, not read: it has no promoted properties, and only subscriptions and routes without a filter pick it up. Its mapping reads it.
Fixed-width text#
fixedwidth reads and writes one record per line, each field at a set position. Nothing in such a file
says where a field ends, so sourceFixedWidth and targetFixedWidth give the layout, as the partner's
specification does:
"sourceFixedWidth": {
"columns": [
{ "name": "type", "start": 1, "length": 1 },
{ "name": "reference", "start": 2, "length": 8 },
{ "name": "amount", "start": 10, "length": 7, "align": "right", "pad": "0" }
],
"skipLines": 1,
"trim": true
}
| Option | Default | |
|---|---|---|
columns |
Each column's name, its start (the line's first character is 1) and its length. Columns can't overlap. |
|
align, pad |
left, a space |
When writing: which side a value keeps to, and what fills the rest. Amounts are usually right with 0. |
skipLines |
0 |
When reading: lines at the top that aren't records, such as a title |
trim |
true |
When reading: take the padding off each value |
Read, the document is a list of rows, as CSV's is. A line shorter than the layout is read as far as it
goes, and blank lines are skipped. When writing, each row has to be an object of the layout's columns
(a nested field becomes a dotted name, as destination.city): a field the layout has no column for is
refused, and a value longer than its column is refused rather than cut, because a cut amount or
reference would be booked without anyone noticing.
Excel#
excel reads and writes Excel workbooks (.xlsx), one sheet at a time. A workbook is binary and a message
in Bitween is text, so it travels as base64: give the receiver that picks it up (S3, Azure Blob, SFTP, FTP)
ResponseEncoding base64, and the upload handler that writes one DataEncoding base64.
| Option | Default | |
|---|---|---|
sheet |
first sheet, or Sheet1 |
sourceExcel: the sheet to read, by name. targetExcel: the name of the one written. |
hasHeader |
true |
Whether the first row names the columns. Without it, columns are named by position: 1, 2, 3. |
Read, the sheet is a list of rows, as CSV's is, and every cell is text: a reference keeps its leading
zeros, numbers are written the invariant way (12.5), cells formatted as dates become ISO 8601
(2026-10-12, or with a time), booleans are true or false, and a formula is the value Excel last
calculated. Empty rows are skipped. The older .xls format isn't read; save it as .xlsx.
Written, the workbook has one sheet, with a header row when hasHeader is on. Text is written as text,
so leading zeros survive, and numbers and booleans as numbers and booleans.
In the editor, the sample of an Excel source is a few rows copied from Excel and pasted, which arrive as
tab-separated text; the preview reads a pasted sample the same way, and shows an Excel output as its rows,
tab-separated, rather than as base64. bitween mapping test sends an .xlsx input file as base64.
Preview#
POST /api/mappingpreviews runs rules against a sample through the same code as the pipeline, without saving anything.
{ "mappingRules": "{ ... }", "sourceDocument": "{ ... }", "partnerId": 7 }
The reply holds outputDocument, contentType, ruleErrors with a target and reason each, and error for problems that stop mapping altogether.
The editor#
The rules editor has three panels.
- Source. Paste a sample, choose the date order, then click or drag a path onto a rule.
- Output. A tree of target fields and lists. Each row picks a source kind and opens the transform, lookup and type steps. Lists have filters and fixed entries. A new list gets its content from + Value, + Field or + List. Choosing + Value makes a list of plain values, whose value is a row of its own.
- Preview. The server's output for the current rules, with failing rows marked.
Build from a sample reads a target sample and adds rules for fields not yet mapped, filling in sources it can recognise. Generate with AI writes every rule from both samples; see below. Preview as chooses a partner for resolving partner values. Save with Ctrl+S or Cmd+S.
The toolbar's format chip, as JSON → XML, opens what the mapping reads and writes: each side's format, and its settings for CSV (separator, header, the byte-order mark Excel wants), fixed-width text (a table of columns, lines to skip, trimming, and on the written side each column's alignment and padding) and Excel (the sheet and the header).
Generate with AI#
Generate with AI asks the provider under Settings, AI for the rules that turn a source sample into an output sample. It opens a dialog with both samples, which are the editor's own, and an optional Instructions box for what the samples don't show, such as codes a lookup needs that the sample doesn't contain. Until Settings has an API key, the dialog says so and links there.
- What is sent: both samples, the instructions, and the names, never the values, of the previewed partner's properties and the global values. Secret properties aren't named either. A rule the model writes from a name is filled in with the real value when the mapping runs, like any other.
- The generated rules replace the editor's rules as one step. The formats and their settings are kept as the editor has them. The toolbar then says what was replaced, which model wrote the rules and how long it took, with Undo beside it. Rules it wrote differently light up for a moment.
- Before answering, the server previews the rules against the source sample and compares the result with the output sample. The toolbar says Matches the output sample, or lists the differences. Each one marks its row, and clicking it in the list brings the row into view.
- A fixed value, or the text of a
concat,replaceordefaultIfEmpty, that the source sample holds as data counts as a difference even when the output matches: it would be the same for every document. A constant the source doesn't have, such as a currency code, is fine. - A blank value in the output sample (empty, only spaces, or null) is a placeholder: any value written there counts as a match. The field itself still has to be written.
- Fix with AI, under the differences, sends the editor's rules back with what was wrong, and asks for corrected ones.
- Samples are limited to 200 KB each and instructions to 4,000 characters. A few records are enough.
- If a sample or a rule changes while it runs, the answer is dropped and the dialog says so, since it was checked against what was sent.
- While it runs, the dialog counts the seconds, usually 10 to 30, and Cancel stops waiting. The model finishes the call it started; its answer is dropped.
- One generation is one model call. A second is made only when the first answer isn't readable rules, with the reason.
- Values from the original document of a response subscription aren't used: the model can't see them. Add those rules by hand.
- Nothing is saved until you save.
POST /api/mappingsuggestions does the work: { mappingRules, sourceDocument, targetDocument, partnerId, refine, instructions }, replying with mappingRules, the preview's outputDocument, ruleErrors and error, differences (each a target path and a reason), matchesTarget and model.
Legacy JSON mapper#
NativeJSONMapper renders a Scriban template over the JSON payload and stores the result as JSON.
The template is compiled once per text and kept, and the payload is read from the parse the exchange already made of it when nothing was written into it. A template renders exactly as before, including __partner__ and __globals__.
Retirement#
The legacy mapper stays in the adapter picker only while at least one subscription uses it. Once the last subscription moves to the rules-based mapper, the option disappears for good. Existing subscriptions keep running and keep their editor.
Template semantics#
- The only property is
ScribanTemplate, defaulting to{}. - The payload must be a JSON object or array. Each key can also be reached with its first letter in lower case, so
CustomerIdandcustomerIdboth work. - Member access on an array reads its first element. A root array is available as
items, and its first element's keys are also available at the top level. | jsonwrites any value as JSON, quoting strings.| to_floatconverts a number or numeric text.- Unknown variables render as empty.
- After rendering, trailing commas are removed and the text must parse as JSON.
- Keys containing dots are expanded into nested objects, so
"a.b": 1becomes{"a": {"b": 1}}. - Partner and global values are available as
__partner__and__globals__.
{
"reference": {{ order.id | json }},
"customer": {{ order.customer | json }},
"account": {{ __partner__?.accountNumber | json }},
"region": {{ __globals__?.regions["default"] | json }},
"total": {{ order.total | to_float | json }}
}
The editor#
The legacy editor draws connections from a source sample tree to an output tree. Each output field takes a source path, a fixed value, a partner property or a global value, with an optional lookup table. Arrays open a dialog for the source array, a filter and per-item fields. A manual mode edits the Scriban text directly, and switching modes converts between the two. Live preview calls POST /api/mappers.
The editor saves ScribanTemplate plus its own layout data in ArrayRules, SourceJson, TargetJson and PartnerId.
The preview endpoint injects partner and global values into each element of a root array, which the pipeline does not do. A preview of a root-array payload can therefore differ from a real run.