CFDL

Operating-company / LBO pack: recurring operating lines, policy-driven working capital, capex, scheduled term debt, cash taxes, and entry/exit — benchmarked in benchmarks/opco/ against an independent month-by-month reference. All lowering is template-driven.

Activation

use pack "opco" version "0.1.0"

Contract types

All contracts accept instance suffixes (opco.revenue_line.saas, opco.revenue_line.services, ...) which suffix the lowered stream names. Growth is annual-compound stepped continuously on the model clock: value(t) = amount * (1 + growth_rate)^(time.t / 12).

Operating lines

  • opco.revenue_lineamount (monthly), optional growth_rate. Stream opco.revenue.recurring.
  • opco.opex_line — same terms. Stream opco.opex.recurring (outflow).
  • opco.working_capital — fixed monthly WC outflow (amount).
  • opco.working_capital_policy — DSO/DPO/DIO-driven: WC(t) = annualized revenue * ar_days/365 + annualized opex * (inv_days - ap_days)/365 from the modeled streams (phase-2 series lookups). Books the full initial WC in the first period, the period-over-period change afterwards, and releases the ending balance in the final period when release_at_end = 1. Terms: ar_days, ap_days, inv_days (all default 0), release_at_end.
  • opco.capex_line — fixed amount (+ growth_rate) plus pct_of_revenue of the modeled revenue streams. Stream opco.capex.

Financing

  • opco.term_debt — scheduled term loan: principal, rate, io_months (default 0), amort_months; optional funded_at_close (default 1) controls the proceeds inflow at term_start. After the IO period the loan amortizes level-pay over amort_months; the remaining balance pays as a balloon at the contract's term_end. Streams opco.debt.proceeds, opco.debt.interest, opco.debt.principal. Cash sweeps and revolvers need per-period persistent state and are not in v0.1.
  • opco.acquisition — purchase price paid at term_start (the equity check when paired with debt proceeds at the same date).

Taxes

  • opco.cash_taxestax_rate on max(0, EBITDA - D&A - interest) per period. EBITDA and interest come from the modeled streams; D&A is a declared deduction (da_monthly, optional da_growth), not a cash stream. No NOL carryforwards (losses floor at zero tax per period; carryforwards need H3-style state). Stream opco.taxes.

Exit

  • opco.exit_multiplebase_value * exit_multiple at the contract's term_start.
  • opco.exit_ebitdaexit_multiple × trailing-12-month EBITDA derived from the modeled streams, net of selling_costs, at term_start.

Metrics

domain.opco.revenue, .ebitda, .ebitda_margin, .capex, .working_capital (net investment; releases net out), .taxes, .debt_service, .fcf (EBITDA − capex − WC − cash taxes; note taxes deduct interest, so this is FCF after the interest tax shield), .fcf_to_debt_service.

Diagnostics (E7xxx)

  • E7001_OPCO_LINE_MISSING_AMOUNT, E7002_OPCO_LINE_INVALID_SCHEDULE, E7003_OPCO_LINE_INVALID_GROWTH
  • E7010_OPCO_WC_MISSING_AMOUNT_OR_RULE, E7011_OPCO_WC_INVALID_SCHEDULE
  • E7020_OPCO_EXIT_MISSING_MULTIPLE, E7021_OPCO_EXIT_INVALID_MULTIPLE, E7022_OPCO_EXIT_MISSING_BASE_VALUE, E7023_OPCO_EXIT_INVALID_SCHEDULE
  • E7024_OPCO_EXIT_EBITDA_INVALID_MULTIPLE
  • E7030_OPCO_DEBT_INVALID_AMORT, E7031_OPCO_DEBT_INVALID_RATE
  • Missing templated terms surface as E5006_MISSING_CONTRACT_TERM.

Not in v0.1 (planned waterfall & capital-stack work)

  • Cash-flow sweeps, revolver draws/paydowns, PIK toggles (need per-period persistent state).
  • NOL carryforwards.
  • Waterfall distributions to the capital stack.

Provenance and determinism

Generated streams carry source contract file/span and generated_by.pack/rule_id; rule ordering, diagnostics ordering, IDs and results are deterministic under identical inputs.

Quick start

A services business bought in an LBO — revenue/opex lines, working capital, capex, term debt:

version 0.1
model "my-buyout"
use pack "opco" version "0.1.0"
time calendar monthly from 2026-01 for 60

entity operating target

contract opco.revenue_line on entity operating.target {
  term 2026-01..2030-12
  terms { amount = 1000000 growth_rate = 0.06 }
}

contract opco.opex_line on entity operating.target {
  term 2026-01..2030-12
  terms { amount = 650000 growth_rate = 0.04 }
}

// Net working capital nets to zero over the full term because
// release_at_end returns the investment at exit — that is the point of the
// term, not an inert stream.
// examples-allow: working_capital.adjustment — released in full at exit
contract opco.working_capital_policy on entity operating.target {
  term 2026-01..2030-12
  terms { ar_days = 45 ap_days = 30 inv_days = 10 release_at_end = 1 }
}

contract opco.capex_line on entity operating.target {
  term 2026-01..2030-12
  terms { amount = 40000 pct_of_revenue = 0.01 }
}

contract opco.term_debt on entity operating.target {
  term 2026-01..2030-12
  terms { principal = 20000000 rate = 0.09 amort_months = 84 }
}

Run it

cfdl compile my-buyout --packs packs --out my-buyout/ir.json
cfdl run my-buyout/ir.json --packs packs --pack opco --out my-buyout/results.json --rate 0.10

Recipes

Scheduled term debt (IO period, level-pay amortization via ipmt/ppmt, balloon at maturity, proceeds at close):

contract opco.term_debt on entity operating.target {
  term 2026-01..2030-12
  terms {
    principal = 14000000
    rate = 0.085
    io_months = 12
    amort_months = 84
  }
}

Trailing-EBITDA exit (the LBO convention — trailing twelve months, not forward):

contract opco.exit_ebitda on entity operating.target {
  term 2030-12..2030-12
  terms { exit_multiple = 8.5 }
}

Full worked model: benchmarks/opco/lbo_buyout/ (validated against an independent recursive reference) and the LBO notebook in examples/notebooks/.

Metrics reference

Computed automatically whenever a model runs with the opco pack, alongside the core metrics (NPV, IRR, MOIC, payback, WAL). Enumerated from the pack definition, so this list is always complete.

MetricTypeBuilt from
domain.opco.revenuemoneyderived
domain.opco.ebitdamoneyderived
domain.opco.ebitda_marginnumberdomain.opco.ebitda ÷ domain.opco.revenue
domain.opco.capexmoneyderived
domain.opco.working_capitalmoneyderived
domain.opco.taxesmoneyderived
domain.opco.debt_servicemoneyderived
domain.opco.fcfmoneyderived
domain.opco.fcf_to_debt_servicenumberdomain.opco.fcf ÷ domain.opco.debt_service

Validations reference

Checked at compile time. Each is a stable diagnostic code that is never renamed or reused; see diagnostics.

CodeRejects
E7001_OPCO_LINE_MISSING_AMOUNTOpCo line is missing required numeric term 'amount'.
E7002_OPCO_LINE_INVALID_SCHEDULEOpCo line term range is missing, invalid, or outside model timeline.
E7003_OPCO_LINE_INVALID_GROWTHOpCo line has invalid 'growth_rate' term.
E7010_OPCO_WC_MISSING_AMOUNT_OR_RULEOpCo working capital requires term 'amount' or a supported rule expression.
E7011_OPCO_WC_INVALID_SCHEDULEOpCo working capital term range is missing, invalid, or outside model timeline.
E7020_OPCO_EXIT_MISSING_MULTIPLEOpCo exit is missing required term 'exit_multiple'.
E7021_OPCO_EXIT_INVALID_MULTIPLEOpCo exit 'exit_multiple' must be greater than 0.
E7022_OPCO_EXIT_MISSING_BASE_VALUEOpCo exit requires numeric term 'base_value'.
E7023_OPCO_EXIT_INVALID_SCHEDULEOpCo exit term range is missing, invalid, or outside model timeline (exit occurs at term_start).
E7024_OPCO_EXIT_EBITDA_INVALID_MULTIPLEOpCo EBITDA exit requires 'exit_multiple' greater than 0.
E7030_OPCO_DEBT_INVALID_AMORTOpCo term debt requires 'amort_months' greater than 0.
E7031_OPCO_DEBT_INVALID_RATEOpCo term debt requires a non-negative 'rate'.

Worked example models

Benchmark cases are validated period-by-period against an independent reference implementation.