Extracting Data from a Contract Note with babylon-table
By Gary Kennedy
July 9, 2026
Contract notes are usually written for people, not software. This example shows how a carefully tagged PDF can do both: remain readable as a document, while allowing babylon-table to extract its tables and turn formatted strings into clean transaction data.
In the previous article, How Hard Is It To Create a PDF Contract Note?, we looked at generating a clean PDF Trade Statement from broker trade email data. In the next article, Open Sourcing babylon-table, we introduced babylon-table, a small dependency-free Java library for ingesting and reshaping tabular data.
This article brings those two ideas together.
The example document is a Babylon Trade Statement. It is our own contract-note-style PDF, generated carefully with tagged PDF structure so that the visible document can also be read as structured data. We generated it ourselves because we wanted a controlled example, not because that is the natural import path.
That matters. Real-world broker contract notes are messier, and extracting data from them requires more powerful techniques. Our point is narrower: contract notes could be written better. They could remain readable as documents while also being friendly to data extraction. In this example, babylon-table extracts the tagged PDF tables and then uses its domain specific language (DSL) to clean, parse, convert, and reshape the extracted string values.
Tagged PDF
The earlier PDF article focused mostly on layout and text extraction order. It did not discuss tagged PDF.
That was not a deliberate omission. When we said in that article that we had not previously worked on writing PDF documents, we really meant it! We were unaware of tagged PDF until a reader contacted us after publication and suggested that it might be a better way to expose the structure of the document.
That suggestion was useful.
A tagged PDF contains a logical structure tree in addition to the visible page content. Instead of a PDF being only positioned text and graphics, parts of the document can be marked with semantic tags. For tabular data, the standard PDF structure is a Table element containing rows and cells, using tags such as Table, THead, TBody, TR, TH, and TD — much like HTML uses table, thead, tbody, tr, th, and td to mark up a table. These are standard PDF structure elements defined by the PDF specification, ISO 32000.
That maps neatly onto the way we wanted to write the Babylon Trade Statement.
The visual PDF can still be laid out as a compact landscape report, but the data sections can also be written as tagged tables. A section such as Trade, Account, Instrument, Charges, Settlement, or Provenance can be represented as a small table. The field labels become table headers, and the values become the first data row.
For example, the visible Trade section might look like a small block of labelled fields on the PDF page. Internally, however, it can be tagged as a simple table:
Table: Trade
Headers: TradeDate, Type, Quantity, Price, Consideration
Row: 5 Feb 2026, Buy, 34,630.1411 units, ZAR 1.03402, ZAR 35,808.12
That gives us the best of both worlds. The PDF remains readable as a document, but it also carries enough structure to be extracted as data directly.
From Tagged PDF to Tables
Once the Trade Statement is written with tagged table sections, extraction becomes much more direct.
Instead of trying to infer a table from x/y coordinates or guess relationships from nearby text, the extractor can read the logical table structure. Each tagged table becomes a babylon-table table. Each TH becomes a column name. Each TD becomes a value.
For this article, we will focus on the Trade table.
The extracted table starts like this:
| TradeDate | Type | Quantity | Price | Consideration |
|---|---|---|---|---|
| 5 Feb 2026 | Buy | 34,630.1411 units | ZAR 1.03402 | ZAR 35,808.12 |
This is already useful, but it is still document-shaped data.
The values are strings because they came from a statement:
Quantityincludes a comma and the wordunitsPriceincludes a currency prefixConsiderationincludes a currency prefix, comma, and decimal amountTradeDateis written as display textTypeis a business word that should become a typed buy/sell value
This is exactly the kind of work the babylon-table DSL is intended to handle.
The DSL Script
Here is the full transformation script:
convert Consideration to Currency by firstIn into Currency
convert Consideration to Decimal by firstIn
convert Quantity to Decimal by firstIn
convert Price to Decimal by firstIn
clean TradeDate using ' ' into TradeDateText
convert TradeDateText to Date using DMY into TradeDate
convert Type to BuySell
retain Type, Quantity, Price, Consideration, Currency, TradeDate
The script is deliberately small.
It does not try to be a general programming language. It describes common table reshaping operations compactly and consistently: convert this column, clean that column, create this output column, retain these columns.
Step 1: Extract the Currency
The first line reads the currency from the Consideration column and writes it into a new column called Currency.
convert Consideration to Currency by firstIn into Currency
The source value is:
ZAR 35,808.12
The first currency-like value in that text is ZAR, so the DSL creates a new Currency column.
After:
| TradeDate | Type | Quantity | Price | Consideration | Currency |
|---|---|---|---|---|---|
| 5 Feb 2026 | Buy | 34,630.1411 units | ZAR 1.03402 | ZAR 35,808.12 | ZAR |
This is a useful example of enriching the table without losing the original column. The Consideration column still contains the full source text, but the currency has been made explicit.
Step 2: Convert Consideration to a Decimal
The next line converts the same Consideration column to a decimal value.
convert Consideration to Decimal by firstIn
This time there is no into clause. That means the result replaces the existing Consideration column.
After:
| TradeDate | Type | Quantity | Price | Consideration | Currency |
|---|---|---|---|---|---|
| 5 Feb 2026 | Buy | 34,630.1411 units | ZAR 1.03402 | 35808.12 | ZAR |
The display string ZAR 35,808.12 has become a BigDecimal value. The currency code and comma separator are gone, and the table now shows the plain decimal representation: 35808.12.
Step 3: Convert Quantity to a Decimal
The quantity is also still a display string:
34,630.1411 units
The DSL line is:
convert Quantity to Decimal by firstIn
The result is a numeric quantity.
After:
| TradeDate | Type | Quantity | Price | Consideration | Currency |
|---|---|---|---|---|---|
| 5 Feb 2026 | Buy | 34630.1411 | ZAR 1.03402 | 35808.12 | ZAR |
Again, the operation is small but important. The document value has become an application value.
Step 4: Convert Price to a Decimal
The price column has the same problem as consideration. It contains a currency prefix and a number:
ZAR 1.03402
The transformation is:
convert Price to Decimal by firstIn
The output is:
| TradeDate | Type | Quantity | Price | Consideration | Currency |
|---|---|---|---|---|---|
| 5 Feb 2026 | Buy | 34630.1411 | 1.03402 | 35808.12 | ZAR |
At this point, the monetary and quantity columns are beginning to look like transaction data rather than PDF text.
Step 5: Clean the Trade Date Text
The trade date is still written as display text:
5 Feb 2026
To parse it, we first remove spaces and write the cleaned value into a temporary column:
clean TradeDate using ' ' into TradeDateText
The result is:
| TradeDate | Type | Quantity | Price | Consideration | Currency | TradeDateText |
|---|---|---|---|---|---|---|
| 5 Feb 2026 | Buy | 34630.1411 | 1.03402 | 35808.12 | ZAR | 5Feb2026 |
This is a small example of a staging column. We do not necessarily want TradeDateText in the final output, but it makes the conversion step explicit and easy to inspect.
Step 6: Convert the Trade Date
Now the cleaned date text can be parsed as a date using day-month-year order:
convert TradeDateText to Date using DMY into TradeDate
The result is an ISO-style date value in the original TradeDate column.
| TradeDate | Type | Quantity | Price | Consideration | Currency | TradeDateText |
|---|---|---|---|---|---|---|
| 2026-02-05 | Buy | 34630.1411 | 1.03402 | 35808.12 | ZAR | 5Feb2026 |
The important point is not just that the date has changed format. It has changed type. The display string 5 Feb 2026 has become a LocalDate, whose natural string representation is the ISO-style date 2026-02-05.
Step 7: Convert Type to BuySell
The trade type is currently the text value Buy.
convert Type to BuySell
In this case the visible output looks the same:
| TradeDate | Type | Quantity | Price | Consideration | Currency | TradeDateText |
|---|---|---|---|---|---|---|
| 2026-02-05 | Buy | 34630.1411 | 1.03402 | 35808.12 | ZAR | 5Feb2026 |
But semantically it is different. The column can now be treated as a buy/sell value rather than arbitrary text.
That distinction matters in financial software. A string can contain almost anything. A BuySell value should only contain a valid trade direction.
Step 8: Retain the Final Columns
The final line chooses the output shape and column order:
retain Type, Quantity, Price, Consideration, Currency, TradeDate
This drops the temporary TradeDateText column and keeps only the columns wanted by the next stage of processing. It also makes the final column order explicit. In this example, TradeDate is deliberately moved to the end of the table.
After:
| Type | Quantity | Price | Consideration | Currency | TradeDate |
|---|---|---|---|---|---|
| Buy | 34630.1411 | 1.03402 | 35808.12 | ZAR | 2026-02-05 |
The result is still a small table, but it is no longer just an extraction from a PDF. It is now a typed, cleaned, application-ready representation of the trade.
Why This Matters
This example is deliberately simple. It has one row and a small number of columns.
But that simplicity is the point.
A contract note is a document. A tagged PDF gives the document a logical structure. babylon-table can extract those tagged tables firstly as string values, preserving the values as they were formatted for the human reader. It can then apply a sequence of small, explicit transformations to produce clean transaction data.
Each step remains visible:
- where the currency came from
- how the decimal values were parsed
- how the display date became a date
- how the trade direction became a business value
- which columns were retained for output
That makes the transformation repeatable, testable, and explainable.
For financial data, this is especially useful. The difficult part is often not the final storage of a transaction. The difficult part is getting from a document, email, CSV file, or broker export into a clean internal record without hiding the logic inside one-off application code.
This is the role of the DSL.
It gives us a small, readable transformation layer between external records and internal data.
A Better Import Loop
In this example, the Babylon Trade Statement was generated by us. That was useful because it let us control the PDF structure and experiment with tagged tables.
But the more interesting idea is the import loop this would allow if broker contract notes were written this way in the first place.
In that world, babylon-table could first extract the tagged PDF tables as string values, preserving the way the values were formatted for the human reader. It could then apply a sequence of small, explicit DSL transformations to produce clean transaction data.
The loop would look more like this:
broker contract note PDF
-> extracted tagged PDF tables
-> string values as shown in the document
-> DSL transformations
-> strongly typed transaction data
That is the important point. The PDF would no longer be just an archive format. It would remain a durable human-readable record, but it would also become a machine-readable record — not as simple as CSV, perhaps, but far closer to structured data than an ordinary PDF.