Skip to main content
The platform ships two subledger types — expenses and journal_proposals — that any runbook can reuse. Most runbooks need more: a rent-roll line, a capital-call schedule, a fixed-asset depreciation entry. These are runbook-owned types. A runbook-owned subledger:
  • Lives inside the runbook bundle (runbooks/<name>/subledgers/<type>.py).
  • Ships its own migration (runbooks/<name>/migrations/NNN_subledger_<type>.sql).
  • Registers with @register_type("<name>", owner_runbook="<runbook-slug>") so the platform registry can disambiguate types across hot-reloads and across runbooks that use the same logical name.

Platform vs runbook-owned

owner_runbook= is the boundary marker — it tells the registry “this type belongs to that runbook” so the same logical name can carry different shapes in different bundles without conflicting, and so hot-reloading the runbook re-registers cleanly.

Worked example — RentalStatementRow

From runbooks/nav-monthly-journals/subledgers/rental_statement.py:
Things to notice:
  • Subclasses Row — gets the standard column block for free (id, entity_id, period, task_id, status, source_ref, validation_errors, raw_payload, timestamps).
  • Domain validators live on the row class — same Pydantic model_validator pattern as ExpenseRow / JournalProposalRow.
  • No propose_for_gl — this type stages period-scoped statement facts; it’s read by nav-monthly-journals’s journal proposer and never posts to the GL directly. GL handoff is opt-in per type.

Migration

The matching migration ships in the runbook bundle, not the platform schema. It applies on tenant Postgres alongside the platform migrations and creates the ledgers.<type> table:
The runbook deploy step picks up runbooks/<slug>/migrations/*.sql and runs them against the tenant database in filename order. Keep the standard column names exact — the Row base relies on them for SubledgerHandle.insert / query / status mutations.

Using the handle

Once registered + migrated, the type is reachable via the same subledger.open(...) flow as platform types:
The handle reads the type from the import-time registry, so the runbook’s subledgers/__init__.py must import rental_statement (or be discovered by the runbook loader) for subledger_open(name="rental_statement", …) to work.

GL handoff (opt-in)

Add a propose_for_gl classmethod when your type does post to the external GL — same shape as ExpenseRow.propose_for_gl or JournalProposalRow.propose_for_gl. Most pure staging types (rent rolls, raw statements) skip it — they’re read by a downstream proposer that produces the actual journal.

When to use the status enum override

The default SubledgerStatus lifecycle (NEEDS_ATTENTION → PENDING → APPROVED → POSTED/REJECTED/EXCLUDED) covers most types. Override only when your type has fundamentally different states — e.g. a capital_calls type with AWAITING_BANK / PAID / DEFAULTED. Override via Pydantic field annotation on your Row subclass; the column name stays status, only the enum type changes.

Subledgers overview

Row base, standard column block, SubledgerStatus lifecycle.

expenses

Platform reference type — full propose_for_gl example.

journal_proposals

Platform reference type — multi-row aggregation pattern.

UI and Temporal signals

HITL row mutations against custom types via workflow signals.