A general ledger is not one thing — it's a system of record plus half a dozen workflows that all read the same accounts, journals, and ledger. The usual result is four engines (a posting DB, a close/reporting warehouse, an FX revaluation job, a reconciliation tool) with sync jobs between them, and a "single source of truth" that is really four copies drifting apart — and, worst of all, a trial balance that doesn't tie in a multi-currency book because each system rounds and translates differently. This suite is the opposite: the whole GL on one DataK3 bucket. Accounts, journals, journal lines and the ledger are the shared double-entry spine; balance-gated posting, period close, subledger reconciliation, FX revaluation and the financial statements are workflows that JOIN that spine directly — one copy of the rows, queried by content (SQL) and by relationship (the account-hierarchy graph).

The problem — and why it matters

A mid-market ERP's GL module lists at six to seven figures a year, and that price buys the four stitched systems above. Each workflow here is worth a tutorial on its own, and has one. The point of the suite is that they compose with zero glue: the close snapshots the same ledger the posting engine wrote; the balance sheet foots the same accounts the close just locked; the FX reval and the trial balance read the same rows. No connector, no nightly export, no reconciliation between products.

But the sharpest payoff is the one a stitched stack quietly gets wrong. In a book that holds a foreign receivable, the trial balance only ties once revaluation has run — and proving that live, to the cent, is the heart of this page. Six base skills, each independently live-validated, compose into the suite:

#SkillWhat it ownsReads (shared master)
1gl/coreaccounts, journals, journal_lines, ledger, account_edges + the gl_accounts graph— (the root)
2gl/journal-entryjournal_auditaccounts, journals, journal_lines, ledger
3gl/period-closeperiods, trial_balanceaccounts, journals, journal_lines, ledger
4gl/subledger-reconciliationsubledger_items, reconciliationledger, accounts
5gl/multi-currencyfx_rates, fx_revalaccounts, journals, journal_lines, ledger
6gl/financial-reportingincome_statement, balance_sheetaccounts, ledger, journal_lines, gl_accounts

This page does not re-teach them — follow each link for the mechanics. Here we show how they assemble into one bucket, and how the multi-currency trial-balance seam is resolved. The machine-readable manifest is the suites/gl contract.

The components teach; the suite ships

Each of the six posts above is a standalone package you can read and run — that is what they are for, and each component's own package download is still exactly that. But the deployable artifact is the suite. code/gl-suite-app is the whole GL as one app: a single FastAPI with a router per component, one canonical models.py covering all 14 tables, the one guarded posting.py write path every journal goes through, and plain imports (no importlib loader) — over one bucket and one dodil-appid pool, so six components mean one sign-in and one bill. That is what you fetch to stamp a customer system; the component posts are what you read to know why each part is shaped the way it is, and how to derive a different customer's version. It ships by the ordinary git cycle (repo → CI → registry → CD): Ship a DODIL app. The tar also carries PLATFORM.md — the platform invariants, so whoever downloads it gets the rules along with the code.

How it composes

Three rules make six skills one GL.

1. One bucket, 14 disjoint tables. Every skill's tables are globally unique names, so they coexist in a single bucket with no collision — 5 core + 1 journal-entry + 2 period-close + 2 subledger-reconciliation + 2 multi-currency + 2 financial-reporting. gl_accounts is a graph, not a table. Installing the suite is installing the six skills, in order, into the same --bucket.

2. Masters are owned once, joined everywhere. gl/core owns the four masters. No other skill re-declares them — they JOIN them. The posting engine writes journal_lines keyed to the same journal_id core owns and re-materializes the same ledger; the close snapshots that ledger; the balance sheet foots the same accounts; the reconciliation ties a control account off the same ledger balance. One row, many readers:

You

In the gl bucket, show the cross-skill joins that prove shared masters: period-close's trial_balance grand total over core's journals, financial-reporting's balance sheet footing core's ledger, and subledger-reconciliation's AP tie-out against core's control account.

DODIL MCP tools called
data_sql
Agent

trial_balance (period-close) grand total = 0.00 across 11 accounts — snapshotted from the same journal_lines core owns. balance_sheet (financial-reporting): assets 969500.00 = liabilities 40000.00 + equity 860000.00 + net income 69500.00, ties to 0.00. reconciliation (subledger-reconciliation): AP control 2100 gl_balance 40000.00 = Σ open subledger_items 40000.00, variance 0.00. Three skills, one ledger, no join table between products.

3. One graph, assembled once. gl_accounts is created from a node table (accounts) and an edge table (account_edges) by a CREATE GRAPH that snapshots its edges at creation — edges added afterwards are invisible. So gl/core inserts nodes + edges but runs no CREATE GRAPH; the single create is deferred to gl/financial-reporting at the tail, the only skill that reads it — and it must run after gl/multi-currency, or the balance sheet foots at stale FX. The account tree points child → parent, so a rollup account's descendant leaves are the incoming direction:

You

In gl, run the single deferred graph assembly (gl_accounts) after core has loaded all account_edges, then roll Total Assets (1000) and Total Revenue (4000) up to their posting leaves over the graph and foot them against the ledger.

DODIL MCP tools called
data_pg
Agent

One CREATE GRAPH gl_accounts over core's fully-loaded account_edges. graph_khop('gl_accounts', 1000, 5, 'in') JOIN ledger → Cash 560000.00, Accounts Receivable 109500.00, Fixed Assets 300000.00 = assets 969500.00. graph_khop('gl_accounts', 4000, 5, 'in') folds in 4100 Product / 4200 Service / 4900 Unrealized FX Gain/Loss (the account multi-currency added) = revenue 479500.00. Bolt agrees: MATCH (root)<-[:account_edges*1..5]-(child) WHERE id(root)=4000 returns 4100/4200/4900.

NOTE

graph_khop() is a top-level-SELECT-only, integer-literal-start table function. The graph plane intercepts SELECT … FROM graph_khop(…); it cannot sit inside INSERT … SELECT, a UNION branch, or take a column as its start node. Pattern: run the graph rollup as a verifying SELECT, then materialize the report with a type-grouped INSERT … ON CONFLICT. The graph proves each leaf's type equals its statement root; the write groups by the denormalized accounts.type.

What composing actually caught

The argument for a suite is usually stated as convenience — one bucket, one deploy. That undersells it. When the six components were finally stood up together on one bucket (2026-09-08), five bugs surfaced that every component had hidden while it ran alone, because alone each one was internally consistent:

  • Four of the six posting routes never re-derived the ledger. A revaluation wrote a balanced FX journal and the ledger kept its old balance — so the 3,230.00 gain existed in journal_lines and in no report. Standalone, multi-currency treated ledger as read-only input and was self-consistent.
  • rederive_ledger overwrote ledger.currency with each component's own FUNCTIONAL_CURRENCY, wiping the EUR marker that the FX revaluation reads to pick its rate pair. It only bit when a second component posted to an account the first one owned.
  • ledger.balance had two incompatible readings — foreign-currency units in one component, the functional-currency signed net everywhere else — so the value was converted twice.
  • And the one worth remembering: the revaluation was not idempotent, and the books still balanced. It read a ledger balance its own journal had already moved, so three runs produced 3,230.00, then 3,278.xx, then 3,326.39 — with the trial balance sitting at 0.00 the entire time. Silently wrong, not loudly broken. A double-entry system can be perfectly balanced and still wrong, which is exactly the failure an auditor is paid to find and a test suite usually is not.

None of these are exotic. They are the ordinary consequence of components that share tables being verified in isolation — and they are the reason this suite is presented as one system rather than six tutorials that happen to sit in the same folder.

The multi-currency trial balance — the seam, resolved

Here is the piece a stitched stack gets wrong, and the reason a GL is the least forgiving workload the pg wire will ever carry.

gl/multi-currency carries an open foreign-currency account at its transaction-currency balance in the single-balance ledger. So after a EUR sale, Accounts Receivable holds 100000.00 EUR — and a naive SUM(ledger.balance) mixes currencies and is simply not a trial balance. The correct convention, and the canonical suite trial balance, is in the functional currency: convert each account's ledger balance at the current period-end rate (functional-currency accounts convert 1:1). That single convention is what lets a multi-currency book tie — and revaluation is exactly what makes it tie.

The accounting story, proven live, in order:

You

In gl, post a EUR sale, show the functional-currency trial balance is off by exactly the unposted FX movement, run the multi-currency revaluation, and show it nets back to 0.00.

DODIL MCP tools called
data_pg
Agent

(a) EUR sale posted: DR Accounts Receivable 108000.00 / CR Product Revenue 108000.00 @ 1.08 — the ledger carries AR at its EUR principal 100000.00, currency EUR. (b) A naive SUM(ledger.balance) reads -8000.00 — meaningless, it mixes EUR and USD. The FUNCTIONAL trial balance at the period-end rate (EUR→USD 1.095000) reads +1500.00 — off by EXACTLY the unposted FX movement: 100000.00 × (1.095000 − 1.080000) = 1500.00. (c) Revaluation posts the balanced FX journal (DR AR 1500.00 / CR Unrealized FX Gain/Loss 1500.00; fx_reval orig 108000.00 → revalued 109500.00, gain 1500.00) and re-strikes AR into the functional currency (109500.00 USD). (d) The functional trial balance now nets to 0.00 — and so does the naive SUM, because every balance is now functional. Revaluation is what makes the multi-currency book tie.

IMPORTANT

Run revaluation before the close. The functional trial balance is off by the unposted FX movement until gl/multi-currency recognizes it — +1500.00 here, exactly 100000.00 × (1.095000 − 1.080000). Revaluation posts the unrealized gain to P&L (Unrealized FX Gain/Loss) and re-strikes the receivable into the functional currency, and only then does the trial-balance snapshot net to 0.00 and the balance sheet foot. That is why the suite's install order puts gl/multi-currency at position 5 and gl/period-close/gl/financial-reporting after it. Money is DECIMAL(18,2), FX rates DECIMAL(18,6), never a float — the 1500.00 gain reads back exact, not 1500.0000004.

One-shot — build the suite (install order matters)

With the DODIL MCP connected, one prompt scaffolds the full suite in order into one bucket:

Scaffold the full GL suite in one bucket (suites/gl).
 
Bucket: gl. Functional currency: USD. Load the demo COA. Install the six base skills IN ORDER into that
one bucket:
  1. gl/core                     — accounts/journals/journal_lines/ledger + account_edges + the demo COA
                                   (the AR leaf 1200 is EUR-denominated; add a 4900 Unrealized FX Gain/Loss
                                   account) + a balanced opening journal + the materialized ledger.
                                   Populate account_edges but DEFER the single CREATE GRAPH.
  2. gl/journal-entry            — balance-gated, idempotent ON CONFLICT posting into core's journals;
                                   re-materialize the ledger. Post a EUR sale here.
  3. gl/multi-currency           — fx_rates + fx_reval; revalue the open EUR balance INTO USD at the
                                   period-end rate BEFORE the close, so the functional trial balance ties.
  4. gl/period-close             — trial-balance snapshot (nets 0.00) + period lock over core.journals.
  5. gl/subledger-reconciliation — tie the AP subledger to control account 2100 (variance 0.00).
  6. gl/financial-reporting      — run the SINGLE CREATE GRAPH gl_accounts, then P&L + balance sheet
                                   rolled up by account type over it (LAST — it reads everything).
 
Then prove the assembly: 14 tables coexist; the functional trial balance is off by the FX movement before
reval and 0.00 after; the balance sheet foots; the AP tie-out variance is 0.00. Ask me the union questions
once (bucket, functional_currency, coa_preset, period) — don't re-ask what composition already answers.

The order is not cosmetic. gl/core owns the masters, so it goes first. gl/multi-currency revalues the open FX balance and must precede the close and the reports — otherwise the trial-balance snapshot and the balance sheet foot at stale rates. gl/financial-reporting runs the single CREATE GRAPH and reads every other skill's output, so it goes last. Want an industry cut? Add overlay: finserv | manufacturing — an additive diff on top of the same 14 tables (a segregation-of-duties close sign-off, or inventory/WIP/COGS variance journals), never a fork.

Building just part of it

You don't have to install all six. To build a subset, install gl/core first — it owns the four masters every workflow reads — then add only the workflow skills you want. Each workflow tutorial names its master dependencies (its contract's consumes block is the machine-readable list): gl/journal-entry posts into journals/journal_lines; gl/subledger-reconciliation reads ledger/accounts. Each workflow can also run fully standalone on an empty bucket — it ships a stub-masters step that creates just the masters it reads — but once two workflows share the same masters, install gl/core once instead of letting each stub its own (avoids divergent seed data and a double-typed ledger). And skip the graph (no CREATE GRAPH) unless you install gl/financial-reporting, which owns the single deferred assembly; every other workflow is pure SQL and needs no graph.

The questions, asked once

Composition removes the redundant asks. The suite interview is short:

  • Asked once (shared): the bucket, the functional_currency (every account, the close snapshot, the reval target and the statements inherit it), the coa_preset (one demo COA drives every skill's Test), and the period (shared by the close, the reval, and the reports).
  • Removed by composition: journal-entry/period-close/multi-currency never ask "seed a COA?" or "stub the masters?" — their accounts, journals and ledger come from core; financial-reporting never asks "build an account hierarchy?" — core loads the edges and reporting runs the one CREATE GRAPH.
  • Still per-workflow (genuine knobs): the retained_earnings_account the close rolls into, the control_accounts map the reconciliation ties, the auto_reverse on the reval. Real best-practice decisions — the overlay defaults them, you tune them.

Verify

The full single-bucket assembly and the FX-seam narrative were live-validated on 2026-09-03 (org IHDIASH, one throwaway bucket, torn down after) — the composition itself, not a re-test of each skill:

# 1) 14 tables from all six skills coexist in ONE bucket, NO name collision
dodil data table list -b gl            # 14 (5 core + 1 journal-entry + 2 period-close + 2 recon + 2 fx + 2 reporting)
 
# 2) the FX seam BEFORE reval — naive sum mixes currencies; functional TB off by exactly the FX movement
dodil data pg -b gl "SELECT ROUND(SUM(l.balance),2) AS naive,
  ROUND(SUM(l.balance * COALESCE(fx.rate,1.0)),2) AS functional
  FROM ledger l LEFT JOIN fx_rates fx ON fx.from_ccy=l.currency AND fx.to_ccy='USD' AND fx.rate_date='2026-09-30'"
#   naive -8000.00 | functional 1500.00   (= 100000.00 × (1.095000 − 1.080000))
 
# 3) AFTER reval — the functional trial balance ties to the cent
#   naive 0.00 | functional 0.00
 
# 4) ONE graph, assembled once; roll Total Assets up to its leaves and foot the ledger
dodil data pg -b gl "SELECT k.node, l.balance FROM graph_khop('gl_accounts', 1000, 5, 'in') k
  JOIN ledger l ON l.account_id = k.node ORDER BY k.node"
#   1100 560000.00 · 1200 109500.00 · 1500 300000.00   (Σ = assets 969500.00)
 
# 5) period lock over shared journals: an operational journal into a CLOSED period is rejected
#   journal 100 (source manual, 2026-09 closed) → absent · journal 101 (2026-10 open) → present
 
# 6) balance sheet foots because the trial balance is 0.00
dodil data sql -b gl "SELECT section, amount FROM balance_sheet WHERE period='2026-09'"
#   assets 969500.00 = liabilities 40000.00 + equity 860000.00 + net income 69500.00

The 14-table coexistence, the shared-master JOINs, the single CREATE GRAPH, the AP tie-out, the period lock, and — the headline — the functional trial balance going off by exactly +1500.00 before reval and back to 0.00 after are all proven live. The Ignite engines each skill deploys (the gl-posting-engine, the gl-period-close-engine, the gl-reval-engine) are validated in their own tutorials; the suite test is the data-plane composition — that six skills share one bucket, one set of masters, one account graph, and one functional-currency trial balance without collision.

Connect your tools

Everything the six skills wrote lives in the one DataK3 bucket, reachable by your own stack — psql and any Postgres driver over the pg wire, a Neo4j driver over Bolt against gl_accounts, gRPC for the table engine. A finance team drives the ledger from psql or a BI tool over the same wire; the trial balance, the statements and the reconciliation are just rows. data connect gl prints the endpoints (pg.uk-lon-1.dodil.io:5432, bolt+s://bolt.uk-lon-1.dodil.io:7687, table-rpc.uk-lon-1.dodil.io:443); point BI, a dashboard, or your app straight at the live rows. Full, live-validated walkthrough: Connect your tools.

Composes

This page is a composition, not a fork:

  • Six base skillsgl/core, gl/journal-entry, gl/period-close, gl/subledger-reconciliation, gl/multi-currency, gl/financial-reporting — each its own tutorial and its own ## Test.
  • The suite manifest (suites/gl) — the ordered install DAG, the shared-master wiring, the single deferred graph rule, and the functional-currency trial-balance rule that resolves the multi-currency seam.
  • Overlays (gl/overlays/*) — additive industry diffs (finserv regulated close, manufacturing cost accounting) that apply on top of the assembled suite.

Read the six to learn each workflow; read this to assemble them into one general ledger — one bucket, one trial balance that ties.