Walkthrough

One feed, four runs.

A commodity futures feed, mapped from one workbook and run four times. The first run is the happy path. The other three show what the engine writes when something is wrong: a typo in the sheet, a value that fails a rule, and a field that throws. In every case the fix is a workbook edit, not a release.

Run 1 · happy path

A row in the sheet becomes a field in the feed.

The target columns are indented. That indentation is the nesting of the output. Beside the feed the engine writes a second file in the same shape that says, for every field, what it read.

The workbook · target section
T1T2T3T4Target expression
Execution
TradeIDtrade.tradeId
Economics
Pricetrade.tradePrice
Amountquantity * (buySell == 'Sell' ? -1 : 1)
Identifier
ExchangegetExchange(instrument.exchange) ?: instrument.exchange
MonthCodeinstrument.ticker.replaceFirst('^[A-Z]+', '').substring(0, 1)
valid.json · what the consumer receives"Execution": {
  "TradeID": "T-20931",
  "Economics": {
    "Price": "81.82",
    "Amount": -1000.0,
    "Identifier": {
      "Exchange": "XNYM",
      "MonthCode": "Z"
    }
  }
}
valid.source.json · why each field holds that value"Execution": {
  "TradeID": "trade.tradeId=T-20931",
  "Economics": {
    "Price": "trade.tradePrice=81.82",
    "Amount": "quantity=1000.0, buySell=Sell",
    "Identifier": {
      "Exchange": "instrument.exchange=NYM,
                   getExchange()=XNYM",
      "MonthCode": "instrument.ticker
                   .replaceFirst(...)=Z"
    }
  }
}
Same shape, field for field

The source file has the same tree as the feed. An analyst lines them up without a developer and without a debugger.

Every input named

Amount read a quantity and a side. Exchange read a raw code and the lookup that mapped it. Lookup and reference results are inputs too.

On every run

This is not reconstructed on request. It is written beside the feed every time the batch runs.

Run 2 · refused workbook

A typo in the sheet never reaches the data.

The source columns declare what exists. When the workbook loads, the engine builds a typed model of that shape and checks every expression against it: property names, method names, argument types. A misspelt path is refused with the row number before a single row is read.

The source, as declared
S1S2Source type
trade
tradeIdString
tradeExecutionDateTimeTimestamp
buySellString
quantityDouble
tradePriceDouble
There is no tradeId2.

Row 7, changed on purpose

Target
Execution.TradeID
Target expression, before
trade.tradeId
Target expression, after
trade.tradeId2
run.sh validate · refused$ run.sh validate
::validate: line=7
  The property [trade.tradeId2]
  is undeclared

No rows were read.
No file was written.
Before any data

Today a typo like this is a failed job somewhere in a pipeline. Here it is a one-line refusal with a row number, before anything flows.

Names, methods, types

The same check catches a wrong method, a wrong argument type and a class that is not on the list.

The fix

Correct row 7 and run again. Nobody opens a build.

Run 3 · failed rule

It says which field failed, and why.

The exchange lookup sheet has no row for one venue code, so the raw code passes through. The validation rule beside the field catches it as the record is produced. The record lands in the invalid file, and the reason file names the field.

The row in the workbook

Target
Identifier.Exchange
Target expression
getExchange(instrument.exchange)
?: instrument.exchange
Validation expression
Exchange == 'NYM'
? 'should be mapped to XNYM' : ''
invalid.json · the document"Identifier": {
  "Type": "ExchangeCode",
  "Exchange": "NYM",
  "Ticker": "CL",
  "MonthCode": "U",
  "YearCode": "2026"
}
invalid.validation.json · why"Identifier": {
  "Type": "",
  "Exchange":
    "should be mapped to XNYM",
  "Ticker": "",
  "MonthCode": "",
  "YearCode": ""
}
As the record is produced

Today validation happens after the fact, somewhere else. Here it runs on the field, with a message an analyst wrote and can act on.

The rest still maps

Every other field in the record is present and correct. The one that failed is the one that is named.

The fix

One row in the lookup sheet: NYM maps to XNYM. The next run passes.

Run 4 · exception

One bad field does not sink the record.

The instrument lookup finds nothing for one trade, so the reference variable is null and the safe navigation in the expression sits one hop too late. The field throws. Everything else in the record still maps, and the exception names the field, not a stack frame.

The row in the workbook

Target
Economics.Amount
Target expression
security.futureInformation?
.contractSize == null
? null
: quantity / security
.futureInformation.contractSize
The fix, one hop earlier
security?.futureInformation?.contractSize
error.json · the document"TradeID": "T-20944",
"Economics": {
  "Type": "CommodityFuture",
  "CommodType": null,
  "Amount": null,
  "Price": "34.11",
  "Identifier": {
    "Exchange": "IFED",
    ...
  }
}
error.exception.json · why"Economics": {
  "Type": "",
  "CommodType": "",
  "Amount": "EvaluationException:
    Cannot get property
    'futureInformation' on null",
  "Price": "",
  ...
}
The field, not the frame

The message says which field, which expression and which null. The trade ID, the price and the exchange all mapped.

Three files, every record in one

Valid, invalid with the rule it failed, or error with the field that threw. Nothing is silent.

The fix

Move the safe navigation one hop earlier. A workbook edit and a review, not a release.

One run, three files

valid.jsonMapped and passed every rule. Beside it, valid.source.json says why.
invalid.jsonWhich field failed which rule. Beside it, invalid.validation.json says which.
error.jsonWhich field threw, and why. Beside it, error.exception.json names the cause.

Run this beside your own feed.

Pick one feed. We map it from the table you already expose, run it for one cycle, and compare the two outputs field by field. You keep the workbook either way.