FP&A Practice Practical writing on financial planning, models as code, and AI agents
home / practice

The knowledge that wouldn't transfer

Thirty years in IT, fifteen in FP&A — and one problem I couldn't solve: how to hand my modeling knowledge to another human. The answer turned out to be a language.

Farseer is used in this example because it is the platform I build and work with.

I’ve been in IT for over thirty years — writing code since elementary school. I belong to the generation that grew up on the C64. By university I was earning my pocket money on serious work — programming for enormous systems with enormous databases, back when knowing how to program was exotic enough to charge for properly. The one limit was geography: in the country I lived in, that knowledge was hard to price at what it was worth elsewhere. Life was still great.

Fifteen years ago I turned toward FP&A. It was new and it was hard in the right way — logically demanding, full of things I didn’t know. Excel was a workbench with infinite options, and I sank happily into night marathons: computing ratios until dawn, building models of new-service launches and their impact on business-as-usual.

For the last six years I’ve worked in Farseer, on a database with its own formula language. The concept is simple — other vendors, like Pigment, apply it too — and the models it produces are powerful and expressive. I invested heavily in learning financial modeling properly, respecting everything data-warehouse science already knows: you can’t just wire formulas together without normalizing the data schema underneath. Data scales only if it’s structured well, and a model scales only if the data does.

The problem

Then came the wall: transferring that knowledge to another person was nearly impossible.

A financial model consisted of a theoretical part you could explain — and a practical part that existed only as the state of a user interface. Making the cognitive connection between the two was, for anyone new to the industry, close to hopeless. I experimented with every profile of person. In the search for people who could hold both halves, we dug through the whole of Croatia — and could count the experts on our fingers.

Technical people struggled to absorb the business knowledge and were awkward in front of clients. Business-minded people struggled with the technical implications of an implementation. Every hire ended up some blend of the two, with no recipe for finding the right one. Training juniors was brutal for the same reason: documenting a financial model was nearly impossible. Sometimes I reached for graphs, sometimes for screenshots, sometimes for helper Excels. None of it survived contact with a real question.

The realization

Breaking my head against this, I understood something that now looks obvious: every set of formulas and models is really one graph — a flow of data you can follow from source to output.

INPUTS CALCULATIONS OUTPUTS "Quantity IN" "Unit Price IN" "Discount % IN" Revenue Net Price "Revenue OUT" reports · dashboards · agents
Every model is one graph: follow the data from inputs, through the calculations, to the outputs.

I tried to write a book about it. It didn’t work. What worked was experimenting: first with drawn graphs — which collapse into spaghetti after the N-th variable — then away from drawing entirely, toward a textual notation from which you can reconstruct the feel of a model without ever seeing a diagram. Step by step, that notation became FML — internally we call it FSL, Farseer Specific Language, our domain-specific subset.

The sketch that replaced the diagrams
variables/revenue.fml
# dimensions need no declaration — naming one in a grain is enough:
# Clients, SKUs, Brands, Years, Months
 
# relations — the FK graph that does the roll-ups
link SKUs -> Brands
 
# variables — each declares its grain, then its formula
"Quantity IN"    { Clients, SKUs, Years, Months }
"Unit Price IN"  { SKUs, Years, Months }
"Revenue OUT"    { Clients, SKUs, Years, Months }
                 = "Quantity IN" * "Unit Price IN"

And the diagrams came back — but as output instead of input. Because the notation carries the whole structure, the tooling can draw the graph from the text, on demand: the picture is always true, and nobody maintains it.

DIMENSIONS & RELATIONS Clients SKUs Brands Months Years link VARIABLES · INPUTS → OUTPUT "Quantity IN" { Clients, SKUs, Years, Months } "Unit Price IN" { SKUs, Years, Months } "Revenue OUT" { Clients, SKUs, Years, Months } = "Quantity IN" × "Unit Price IN"
The same file, drawn by the tooling: relations on top; below, the formula flow from inputs to output — each variable carrying its grain.

The same model, three ways

A revenue module is a toy. So take the smallest thing every finance person recognises — a three-statement model, where the P&L feeds the balance sheet, the balance sheet feeds the cash flow, and the cash flow has to come back and agree with the balance sheet.

Here it is as a spreadsheet. Nothing is wrong with it: the numbers are right, the formulas are right, and a competent analyst built it.

fx =C9+C7-(C11-B11)+(C13-B13) A B · Jan C · Feb D · Mar 4PROFIT & LOSS 5Revenue 1,200 1,320 1,450 6Cost of sales 540 594 653 7Depreciation 42 44 46 8Operating expenses 380 395 410 9Net income 238 287 341 10BALANCE SHEET 11Receivables 400 440 483 12Property, plant & equipment 2,100 2,156 2,210 13Payables 260 286 314 14Equity 2,550 2,837 3,178 15Cash 310 527 799 16CASH FLOW 17Operating — 317 372 18Investing — (100) (100) 19Net change in cash — 217 272 the selected cell is one of 132 formulas · nothing says which rows are entered and which are derived add a fourth month → insert a column, then repair every formula that crosses it add a second entity → copy the sheet, and now there are two truths
The spreadsheet is correct. What it cannot tell you is which numbers are entered, which are derived, and what happens when the business changes shape.

Now the same model as text — in FML, with Farseer’s own formula language on the right-hand side of every =. Every line declares where a number lives (its grain), whether a human types it (IN) or the model computes it, and how balances roll forward.

The whole model — three statements that close
model/three-statement.fml
# --- inputs: the only cells a planner types into ---
"Revenue IN"             { Months }
"Cost of sales IN"       { Months }
"Opex IN"                { Months }
"Capex IN"               { Months }
"DSO days IN"            { Months }
"DPO days IN"            { Months }
"Depreciation rate IN"   { Months }
 
# --- imports: opening balances, loaded from the ledger ---
IMPORT_PPE_opening       { Months }                                 @readonly
IMPORT_Equity_opening    { Months }                                 @readonly
IMPORT_Cash_opening      { Months }                                 @readonly
 
# --- profit & loss ---
"Depreciation"           { Months } = delay("PPE", 1) * "Depreciation rate IN"   @readonly
"Net income OUT"         { Months } = "Revenue IN" - "Cost of sales IN"
                                      - "Opex IN" - "Depreciation"  @readonly
 
# --- balance sheet: opening balance + what moved this month ---
"Receivables"            { Months } = "Revenue IN" * "DSO days IN" / 30
                                              @rollup last  @readonly
"Payables"               { Months } = "Cost of sales IN" * "DPO days IN" / 30
                                              @rollup last  @readonly
"PPE"                    { Months } = cumsum("Capex IN" - "Depreciation", 0,
                                             IMPORT_PPE_opening)
                                              @rollup last  @readonly
"Equity"                 { Months } = cumsum("Net income OUT", 0,
                                             IMPORT_Equity_opening)
                                              @rollup last  @readonly
"Cash"                   { Months } = cumsum("Net change in cash OUT", 0,
                                             IMPORT_Cash_opening)
                                              @rollup last  @readonly
 
# --- cash flow: the indirect walk, straight off the two above ---
"CF operating"           { Months } = "Net income OUT" + "Depreciation"
                                      - ("Receivables" - delay("Receivables", 1))
                                      + ("Payables"    - delay("Payables", 1))
                                                                  @readonly
"CF investing"           { Months } = -"Capex IN"                 @readonly
"Net change in cash OUT" { Months } = "CF operating" + "CF investing"     @readonly
 
# --- the control: assets - liabilities - equity, must read zero ---
"BS control"             { Months } = round("Cash" + "Receivables" + "PPE"
                                            - "Payables" - "Equity", 1)
                                                                  @readonly

Thirty-odd lines, and every question the spreadsheet couldn’t answer is answered in place. Which numbers are entered? The seven marked IN — everything else is @readonly, computed by the model, and the platform enforces it. What does “the statements close” actually mean? The last line — a control that must read zero in every period, or something is wrong. What happens when a second entity arrives? You add it to the grain, once, and every balance rolls forward per entity.

Three Farseer formula functions carry most of the weight here — the same ones a modeller uses in the platform’s UI:

  • cumsum(movement, 0, opening) — the roll-forward. It runs through the timeline and accumulates the movement period by period, starting from the opening balance handed to it as the third argument. The middle argument is a growth factor: 0 means plain accumulation, while a rate there compounds — which is how you model a loan or an interest-bearing balance with the same function.
  • delay(X, 1) — the same variable one month earlier. It does two jobs in this model: depreciation is charged on last month’s PP&E rather than this month’s (which also keeps the formula from chasing its own tail), and the cash flow needs the change in a balance, which is simply this month minus delay(…, 1).
  • round(X, 1) — rounds to one decimal, so the control reads a clean zero instead of floating-point dust from the divisions above it.

And because the structure is written down, the picture draws itself:

INPUTS PROFIT & LOSS CASH FLOW BALANCE SHEET "Revenue IN" "Cost of sales IN" "Opex IN" "Capex IN" "DSO / DPO days IN" "Net income OUT" { Months } "Depreciation" delay("PPE", 1) × rate "CF operating" "CF investing" working-capital deltas "Net change in cash OUT" { Months } "Cash" · "Receivables" · "PPE" · "Payables" · "Equity" every line: cumsum(movement, 0, opening) · @rollup last { Months } "BS control" = assets − liabilities − equity → 0
The same model as a graph, generated from the text: net income lands in equity, the cash flow lands in cash, and the control at the bottom is what "the statements close" actually means.

A junior can read that. A senior can review it — in git, as a diff, exactly the way a developer reviews source code: two versions side by side, every change visible, with the reason for it written next to the line that changed. The knowledge that refused to transfer for a decade suddenly had a carrier.

And then it turned out humans weren’t the most important readers. What happened when I handed the language to an AI is the next article.

ZD
Zrinko Dolic

Co-founder of Farseer

15+ years in FP&A and planning. I write about models as code, AI agents, and lessons from enterprise rollouts. Farseer appears in my examples because it is the platform I build and work with.