GAEB tenders, bids and orders, read by the book in Rust.
gaebboss reads GAEB DA XML 3.0 to 3.3 in every phase from X80 to X89, validates the bill of quantities, writes schema-valid X83, X84 and X86, and turns a tender into a priced bid. The typed model, the readers, the writers and the per-phase rules are generated from the official gaeb.de schemas; money and quantities are fixed-point. One Rust core behind a CLI and Python bindings. MIT OR Apache-2.0.
Prebuilt abi3 wheels for CPython 3.12+ on Linux, macOS and Windows, no toolchain required.
The command line
From tender to bid in three commands.
The gaebboss binary ships info, items, text, json, validate, roundtrip and bid. Below it reads the official BVBS sample tender, prices it from a CSV of unit prices and a TOML file with the bidder, and checks the resulting X84 against the tender it answers.
prices.csv is OZ;unit price; a - marks an item not offered and an optional third column carries the quantity of a free-quantity item. validate exits 1 on errors, so it drops straight into CI.

Python and Rust
The same engine, one import away.
pip install gaebboss gives immutable documents that can be shared between threads. Reading, item walks, validation, writing and bidding run with the GIL released, and every GAEB number arrives as a decimal.Decimal built from its exact text. Floats are refused where money goes in.
from decimal import Decimal
import gaebboss
tender = gaebboss.Document("tender.X83") # or Document(data=raw_bytes, name="tender.X83")
print(tender.version, tender.phase, tender.project.name)
for item in tender.items(): # items(kind="counting") for what the total counts
print(item.oz, item.kind, item.quantity, item.unit, item.short_text)
for warning in tender.warnings: # every tolerated defect, with its byte offset
print(warning.code, warning.offset, warning.message)
bidder = {"name1": "Bau GmbH", "street": "Weg 1", "pcode": "12345", "city": "Berlin"}
bid = gaebboss.Bid.from_tender(tender, bidder)
for item in tender.items():
bid.price(item.id, Decimal("12.50"))
bid.vat_percent(Decimal("19"))
x84 = bid.finish() # raises on unpriced items
open("bid.X84", "wb").write(x84.write())
print(x84.validate_against(tender))use gaebboss_core::bid::{Bid, Bidder};
use gaebboss_core::decimal::Decimal;
use gaebboss_core::Document;
let tender = Document::open(Path::new("tender.X83"))?;
for item in tender.items() {
println!("{} {:?} {}", item.oz, item.quantity(), item.short_text());
}
let bidder = Bidder { name1: "Bau GmbH".into(), ..Bidder::default() };
let mut bid = Bid::from_tender(&tender, bidder)?;
for item in tender.items() {
bid.price(item.id(), Decimal::parse("12.50")?)?;
}
let x84 = bid.finish()?;
std::fs::write("bid.X84", x84.write(&x84.x84_options())?)?;The rules
What a receiving AVA system would check.
Bid::from_tender applies unit prices, price components, not-offered marks, free quantities, bidder text complements, discount and VAT, and writes the X84 the way the “Regeln für X84” demand it. The worked example of the Fachdokumentation (73.789 × 9.77 = 720.92, 710.11 after 1.5 %) is a unit test.
From tender to bid
- Project, bill and structure taken over unchanged from the tender.
CTRmade mandatory;OWN, requester, site and owner texts dropped.- Totals on every category and on the bill.
- No unit price on items not offered, a quantity only on free-quantity items, no item total on items that do not count.
- Item totals round2(Qty × UP); discounts apply to the rounded total and round again; categories sum rounded items.
- Answered in the tender's own version: 3.2 and 3.3 in kind, 3.0 and 3.1 in 3.2.
Validation
- Elements a phase does not allow or requires.
- Duplicate or dangling ids; Ordnungszahlen that are not unique, not ascending or too long.
- Decimals with more places than the schema gives them.
- Item totals that do not match quantity times price; category and bill totals that do not match their items.
- Unit prices on items not offered, quantities on non-free items in an X84.
--tenderadds the X84 against its X83: items missing or added, ids and structure that differ.
Lenient, and it says so
- BOM, UTF-16, Latin-1 and CP1252 input normalized and repaired.
- Mismatched tags closed; nesting past 256 levels refused with an error.
- Unknown vendor elements kept and written back verbatim.
- Every tolerated defect a
Warningwith its byte offset. - Detected and named, not parsed: GAEB 90, GAEB 2000, DA XML 2.x, ÖNORM.
- Panics denied by the workspace lints.
Benchmarks
The fastest GAEB reader we could measure.
Against pygaeb, the fullest open GAEB reader, and raw lxml over 40 real-world files from a 164-file corpus of third-party exports (22 MB, DA XML 3.0 to 3.3), best of three per file, single-threaded, from the installed Python packages.
Opening and parsing real files from Python.
On the same files, open and parse.
Which builds no GAEB model at all.
The 75 MB X81 in the corpus, read end to end.
Open and parse
40 real-world files · MB/s, higher is better
The lxml row parses the file and iterates items reading RNoPart, Qty and span text: no Ordnungszahl, no kind, no decimals. It is a floor for any lxml-based reader, not a competitor.
Walk every item
Ordnungszahl, quantity and short text · MB/s
pygaeb reads every text eagerly, so its two rows coincide: parsing already does the work.
| Stage | Dense synthetic 10 MB X83 | 75 MB real X81 |
|---|---|---|
| tokenize | 2.36 GiB/s | 15.2 GiB/s |
| typed parse | 675 MiB/s | 2.59 GiB/s |
| write back | 497 MiB/s |
Measured on an aarch64 workstation (Cortex-X925, 20 cores). Over the whole corpus, cargo xtask corpus reads 189.6 MB at 866 MB/s with zero errors and writes it back at 421 MB/s. The corpus is not redistributable; reproduce on your own files with benchmarks/bench.py.
Generated from the standard
Built from the XSDs, checked against them.
The typed model is one struct per schema type, generated from the 3.3 Lib and extended with what only 3.1 and 3.2 carry. Strings are spans into the input, numbers are Decimal { mantissa, scale }, and every struct keeps the children it does not know as raw nodes that are written back verbatim.
elements of DA XML 3.1, 3.2 and 3.3 typed, read and written straight from the vendored XSDs.
real-world corpus files plus every official sample read with zero errors; writing what was read gives the same bytes back.
X81 to X84 files written from the official fixtures validate against the official XSDs with lxml.
phases, X80 to X89, read losslessly in DA XML 3.0 to 3.3; cargo xtask gen --check fails CI when the code no longer matches the schemas.
gaebboss-core
The engine: bytes, tape, decimal, generated model, readers, writers, profiles, bill semantics, texts, validation, bid, JSON.
gaebboss-cli
The gaebboss binary.
gaebboss
The Python package: pyo3 bindings in abi3 wheels for CPython 3.12+.
Scope: read DA XML 3.0 to 3.3, phases X80 to X89; write X83, X84 and X86 in 3.2 and 3.3 with phase rules. The trade, cost and quantity phases are typed but carry no business rules. The schema files remain the work of GAEB / DIN. Limitations →