Workflow Automation · BraivIQ AI Engineering Playbook
Workflow Automation Code Samples: A Daily Reconciliation On Polars 2.0 With A Signed-Off Break Report
These workflow automation code samples build a daily positions reconciliation that a reviewer can sign off and an auditor can reproduce. Polars 2.0, released on 6 October 2026, turns on out-of-core processing by default and drops input order in more places where a query does not ask for it, so the pipeline sorts explicitly, validates join keys one to one and refuses a file whose columns have changed. The break report gets a SHA-256 fingerprint, and a named reviewer must record a decision and reason for every break before the sign-off is written to a hash-chained log.
Published · Updated · 11 min read · By BraivIQ Engineering
Key takeaways
- Polars 2.0 was released on 6 October 2026. Its breaking changes include out-of-core processing on by default and several operations that no longer keep input order when the query does not ask for it. Never rely on the order a join returns.
- Sort explicitly, validate join keys one to one and keep quantities as decimals, so the same inputs always give the same report.
- Check the header of every input file and stop the run if a column appears or disappears. Polars 2.0 raises an error late for extra CSV columns, so check first.
- Fingerprint the report and bind the sign-off to that fingerprint. A regenerated or edited report needs a fresh sign-off.
6 Oct 2026 - Release date of Polars 2.0.0 · 3 - Break types in the sample: missing at custodian, missing internally, quantity mismatch · 1:1 - Join key validation, so a duplicated row fails the run instead of doubling a position · 10 - Minimum characters for a reviewer's reason, enough for a colleague to follow
A reconciliation you can automate is one that gives the same answer twice. These workflow automation code samples build that in Polars 2.0: load the portfolio system and custodian files with a fixed schema, refuse any file whose columns changed, join them with validated keys, classify each break and sort the result. The report is fingerprinted, and nothing is closed until a named reviewer records a decision and reason for every break.
Both samples run on Python 3.14.4 and 3.11.14 with polars 2.0.0 and pass mypy 2.4.0 in strict mode. The accounts and quantities are sample data.
What changed in Polars 2.0 that matters for reconciliations?
The 2.0.0 release notes list out-of-core processing on by default, with 80% of available memory as the threshold, and several changes that stop keeping input order where the query does not observe it, such as order-insensitive windows and Parquet and IPC scans. None of this is a bug. It is the engine using the freedom it always had. A break report that silently changes row order between runs is harder to review and impossible to fingerprint, so make the order explicit.
pyarrow 26.0.0, released on 9 October, also drops Python 3.10, which matters if your reconciliation server still runs it.
What do workflow automation code samples for a reconciliation look like?
recon.py"""Positions reconciliation on Polars 2.0: portfolio system against the custodian file.
Polars 2.0 (6 October 2026) turns on out-of-core processing by default and, in several places, stops
keeping input order wherever the query does not ask for it. A join's output order was never a promise.
A break report must be reproducible, so this pipeline sorts explicitly, validates the join keys and
refuses a file whose columns have changed.
Requires polars 2.0.0. Tested on Python 3.14.4 and 3.11.14 with mypy --strict. Sample data only.
"""
from __future__ import annotations
import hashlib
from pathlib import Path
import polars as pl
KEYS = ["account", "isin"]
SCHEMA: dict[str, pl.DataType] = {"account": pl.String(), "isin": pl.String(), "quantity": pl.Decimal(18, 4)}
def load(path: Path, side: str) -> pl.LazyFrame:
with path.open(encoding="utf-8") as fh:
header = fh.readline().strip().split(",")
if header != list(SCHEMA):
raise ValueError(f"{path}: expected columns {list(SCHEMA)}, found {header}")
return pl.scan_csv(path, schema=SCHEMA).rename({"quantity": f"qty_{side}"})
def breaks(internal: Path, custodian: Path) -> pl.DataFrame:
joined = load(internal, "internal").join(
load(custodian, "custodian"), on=KEYS, how="full", coalesce=True, validate="1:1",
)
return (
joined.with_columns(
pl.when(pl.col("qty_custodian").is_null()).then(pl.lit("missing_at_custodian"))
.when(pl.col("qty_internal").is_null()).then(pl.lit("missing_internally"))
.when(pl.col("qty_internal") != pl.col("qty_custodian")).then(pl.lit("quantity_mismatch"))
.otherwise(pl.lit(None)).alias("break_type"),
(pl.col("qty_internal").fill_null(0) - pl.col("qty_custodian").fill_null(0)).alias("difference"),
)
.filter(pl.col("break_type").is_not_null())
.sort(KEYS) # explicit order: never rely on the order a join happens to return
.collect()
)
def fingerprint(df: pl.DataFrame) -> str:
"""Same inputs, same report, same hash: what a reviewer signs is what the auditor sees."""
return hashlib.sha256(df.write_csv().encode()).hexdigest()
if __name__ == "__main__":
Path("internal.csv").write_text(
"account,isin,quantity\n"
"ACC-20417,GB00B03MLX29,1000\nACC-20417,GB0002634946,250\n"
"ACC-11893,GB0007980591,40\nACC-30001,GB00B03MLX29,75.5\n")
Path("custodian.csv").write_text(
"account,isin,quantity\n"
"ACC-20417,GB00B03MLX29,1500\nACC-20417,GB0002634946,250\n"
"ACC-30001,GB00B03MLX29,75.5\nACC-44120,GB0002634946,10\n")
report = breaks(Path("internal.csv"), Path("custodian.csv"))
print(report)
print("fingerprint:", fingerprint(report)[:16], "| repeat:", fingerprint(breaks(Path("internal.csv"),
Path("custodian.csv")))[:16])
Path("custodian_v2.csv").write_text("account,isin,quantity,price\nACC-20417,GB00B03MLX29,1500,3600\n")
try:
breaks(Path("internal.csv"), Path("custodian_v2.csv"))
except ValueError as exc:
print("refused:", exc)Requires polars 2.0.0. Tested on Python 3.14.4 and 3.11.14 with mypy 2.4.0 --strict. Sample data.
The run finds three breaks: ACC-11893 is missing at the custodian, ACC-20417 differs by 500 units and ACC-44120 is missing internally. Running it twice gives the same fingerprint. A custodian file with an extra price column is refused with a message naming the expected and found columns.
How do you sign off a break report?
break_signoff.py"""Sign-off for the daily break report: a named reviewer resolves every break with a reason, and the
sign-off is bound to the report's fingerprint so a regenerated or edited report needs a fresh sign-off.
Requires polars 2.0.0 (via recon.py). Tested on Python 3.14.4 and 3.11.14 with mypy --strict.
"""
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Literal
import polars as pl
from recon import breaks, fingerprint
Resolution = Literal["book_correction", "chase_counterparty", "timing_difference", "escalate"]
@dataclass(frozen=True)
class Signoff:
account: str
isin: str
resolution: Resolution
reason: str
def sign_off(report: pl.DataFrame, reviewer: str, decisions: list[Signoff], log: Path) -> str:
open_breaks = {(str(r["account"]), str(r["isin"])) for r in report.iter_rows(named=True)}
decided = {(d.account, d.isin) for d in decisions}
missing = sorted(open_breaks - decided)
if missing:
raise ValueError(f"no decision for {missing}")
if any(len(d.reason.strip()) < 10 for d in decisions):
raise ValueError("every decision needs a reason a colleague could follow")
lines = log.read_text().splitlines() if log.exists() else []
prev = json.loads(lines[-1])["hash"] if lines else "0" * 64
body = {"report_fingerprint": fingerprint(report), "reviewer": reviewer, "prev": prev,
"at": datetime.now(timezone.utc).isoformat(),
"decisions": [vars(d) for d in sorted(decisions, key=lambda d: (d.account, d.isin))]}
digest = hashlib.sha256(json.dumps(body, sort_keys=True).encode()).hexdigest()
with log.open("a", encoding="utf-8") as fh:
fh.write(json.dumps({**body, "hash": digest}) + "\n")
return digest
if __name__ == "__main__":
report = breaks(Path("internal.csv"), Path("custodian.csv")) # files written by recon.py
log = Path("signoffs.jsonl")
log.unlink(missing_ok=True)
partial = [Signoff("ACC-20417", "GB00B03MLX29", "book_correction",
"Scrip dividend of 500 units credited at custodian on 14 Oct, not yet booked")]
try:
sign_off(report, "ops.lead@example.co.uk", partial, log)
except ValueError as exc:
print("refused:", exc)
complete = partial + [
Signoff("ACC-11893", "GB0007980591", "chase_counterparty", "Custodian file omits the account, chaser drafted"),
Signoff("ACC-44120", "GB0002634946", "escalate", "Account not in portfolio system, needs onboarding review"),
]
print("signed:", sign_off(report, "ops.lead@example.co.uk", complete, log)[:16])Requires polars 2.0.0 via recon.py. Tested on Python 3.14.4 and 3.11.14 with mypy 2.4.0 --strict.
A sign-off covering only one of the three breaks is refused, listing the two without a decision. With all three decided, the sign-off is written with the report fingerprint, the reviewer and each reason, chained to the previous entry. This is where an AI agent can help: drafting the likely cause for each break from custodian notes and corporate action feeds, which the reviewer accepts or changes. The decision stays with the person.
What breaks in production?
- **Float quantities.** 75.5 read as a float can differ in the last digit after arithmetic. Read quantities as decimals, as the schema here does.
- **Duplicate rows upstream.** Without validate set to 1:1 a duplicated custodian row doubles a position silently.
- **Timing breaks that never clear.** Track the age of each break and escalate anything older than your tolerance.
- **Sign-offs on a different report.** If someone reruns the job after sign-off, the fingerprint changes and the old sign-off no longer applies, which is the point.
What are the alternatives to Polars for this?
| Option | Good fit | Trade-off |
|---|---|---|
| pandas | Small files and teams that already know it | Slower on large files, and the same ordering care applies |
| DuckDB | Teams who prefer SQL | Also needs an explicit ORDER BY for a stable report |
| A reconciliation platform | Many feeds and matching rules | Licence cost and less control over the logic |
| Polars 2.0, as above | Large files and typed pipelines | Breaking changes to test when upgrading from 1.x |
Where should an operations team start?
Pick the reconciliation that takes the longest each morning and run the code above against last week's files beside the current process. Our blog on reconciliation automation with AI agents covers the exception side, and our durable approvals playbook covers sign-offs that wait days. Our Workflow Automation London team builds the daily mismatch report this way, and a 14-day Proof Run is the quickest way to see it on your own data.
Frequently asked questions
Does Polars 2.0 change the order of join results?
Polars never promised join output order unless you ask for it, and 2.0 stops keeping input order in more places where a query does not observe it. Sort explicitly before writing any report you need to reproduce or fingerprint.
Why fingerprint a reconciliation report?
So the sign-off is bound to exactly what the reviewer saw. If the report is regenerated or edited, the SHA-256 changes and a new sign-off is needed, which gives auditors a clear link between decision and data.
Can AI close reconciliation breaks on its own?
In the design shown here it does not. An agent can draft the likely cause and a suggested resolution, but a named reviewer records the decision and reason for every break before anything is closed.
References
- Polars (GitHub), "Python Polars 2.0.0 release notes", 6 October 2026. https://github.com/pola-rs/polars/releases/tag/py-2.0.0
- Apache Arrow, "Apache Arrow 26.0.0 release", 9 October 2026. https://arrow.apache.org/release/26.0.0.html
- UK Accelerated Settlement Taskforce, "T+1 implementation toolkit", 25 September 2026. https://acceleratedsettlement.co.uk/tplusoneimplementationtoolkit/
- PyPI, "polars 2.0.0", 6 October 2026. https://pypi.org/project/polars/2.0.0/