-
Notifications
You must be signed in to change notification settings - Fork 6
Reporting and Rendering
RoboSystems renders financial statements once, on the server, from atomic facts — and ships the result to any consumer as light JSON. The browser displays pre-computed rows; it never walks a calculation graph. This page explains the rendering engine, the conceptual model behind it, and how to request a rendered report.
- Overview
- The Calc-DAG Model
- What the Renderer Derives for You
- Reporting Style Decides Which Structures the Renderer Walks
- The Six View Projections
- Guard Rails
- The Report Is the Package
- How to Request a Rendered Report
- The BlockView Frontend
- How to Contribute: Writing Blocks
- Related Documentation
- Support
Three ideas run through the entire reporting layer:
- One renderer, many sources. A single Python algorithm renders statements for SEC filings, materialized tenant graphs, and live OLTP ledgers. Because the calc walk lives on the server, every consumer — the dashboard, an MCP-driven AI assistant, the Python client — gets identical numbers. One line of frontend code replaces the equivalent of hundreds of lines of client-side calculation.
- One block, six shapes. An Information Block is a data envelope around a set of facts. A View is one lens over that envelope. The rendered statement (rows, subtotals, period columns) is just one View; there are five others (see The Six View Projections).
- Facts are structure-agnostic; a statement is a walk over a structure. The same flat bundle of facts can be rendered as a GAAP Balance Sheet or a tax-basis Balance Sheet — two different walks over the same facts. Nothing about a fact knows what statement it belongs to.
Rendering happens in two phases:
- Phase 1 — fact generation is structure-agnostic. It reads mapped chart-of-accounts balances, derives earnings, cash-flow, and subtotal facts, and emits a flat bundle.
- Phase 2 — structure rendering walks a presentation tree and a calculation DAG over those facts to produce ordered rows with depth, subtotals, and one value per period column.
Because the two phases are separate, the same facts feed any number of structures.
A financial statement is a tree of roll-ups: leaves sum into subtotals, subtotals sum into bigger subtotals, all the way to a root (Assets, Net Income). Internally this is a calculation DAG — a parent equals the weighted sum of its children, recursively, down to the leaves.
The single most important rule:
Your chart of accounts maps only to leaves. Subtotals (
Assets,Revenues,GrossProfit) are always derived, never mapping targets — and they are also persisted as facts so that rules can bind to them.
This keeps the model honest. If a subtotal could be mapped directly, two sources of truth would exist for the same number — the mapped value and the computed roll-up — and they could disagree. By forbidding it, the roll-up is the only source of truth for any subtotal.
A practical consequence: mapping a CoA account to a subtotal concept means the fact lands on a dead branch and never renders. Every mapping target must reach a network root through the calc DAG. A catch-all "Other" leaf exists per section so that genuinely unclassified balances still foot.
Phase 1 emits more facts than you mapped. Four derivations are worth understanding:
-
Auto-derived Retained Earnings. Net Income (the sum of revenue minus expenses, netting dividends and buybacks) is folded into the equity close-target concept at render time, so
Assets = Liabilities + Equityholds without a posted closing journal entry. This is the QuickBooks pattern, and it makes backdating safe: Retained Earnings is recomputed on every render, so a backdated transaction can never leave a stale balance behind. - Net Income as a standalone fact. The same earnings figure is emitted as its own fact so the Income Statement bottom line and the Equity roll-forward agree by construction.
-
Flow-as-fact cash flow. Investing and financing cash-flow facts are emitted directly from each ledger line's flow tag (
flow_element_id). Operating cash flow is derived from period-over-period balance-sheet deltas (the indirect method). This is why untagged accounting data still renders a cash-flow statement — provided accounts are mapped at the grain the arcs key on (for example, PP&E Gross for capex). - PP&E net synthesis. Gross property and accumulated depreciation roll up into a net line so the Balance Sheet presents the figure readers expect.
All of these are computed, not stored on the ledger — which is exactly why backdating, restatement, and re-mapping stay safe: the next render recomputes everything from current facts.
Two companies can hold the same facts and still present different statements — different ordering, different subtotal layout, a single-step versus multi-step income statement. That choice is the Reporting Style.
Every tenant picks a Reporting Style at provision time (the field is non-nullable). The Style maps to one Network — a presentation tree — per statement type. At render time the engine does one deterministic join: Style → Network → the leaves the calc DAG rolls up. It never guesses which structure to walk.
Reporting Style ──▶ Network (per statement type) ──▶ leaves ──▶ calc DAG roll-up
(chosen once) (presentation tree) (your CoA) (derived subtotals)
Switching a Style later changes future renders but never retroactively rewrites a filed report: each saved FactSet pins its own structure at create time, so frozen reports keep the presentation they were filed with.
An Information Block carries facts plus the structural metadata around them. A View is one projection of that envelope. The same block supports six:
| View | What it shows | Computed where |
|---|---|---|
| Rendering | Ordered statement rows with subtotals, depth, and one value per period | Server (needs the calc walk) |
| Fact Table | The raw fact list (element, period, unit, value) | Projection of atoms in the envelope |
| Report Elements | The element/concept definitions referenced | Projection of atoms in the envelope |
| Verification Results | Rule-engine outcomes for the block | Projection of atoms in the envelope |
| Model Structure | The presentation/calculation tree itself | Projection of atoms in the envelope |
| Business Rules | The rules bound to the block | Projection of atoms in the envelope |
Only Rendering is server-computed — it is the one View that requires walking the structure and footing the subtotals. The other five are projections of atom lists the envelope already carries, so a client can assemble them without a round trip.
In the GraphQL envelope, the rendered View lives at view.rendering and is populated for the statement family (Balance Sheet, Income Statement, Cash Flow, Equity). Non-statement blocks such as Schedules group their facts by period and do not carry a server-computed rendering.
A RenderingLite payload is intentionally small:
RenderingLite
rows[] element_name, classification, depth, is_subtotal, values[] (one per period)
periods[] label, start, end
validation passed, checks[], failures[], warnings[]
unmapped_count
Each row's classification follows FASB SFAC 6 (asset, liability, equity, revenue, expense), depth drives indentation, and is_subtotal marks derived roll-up rows. The browser renders this directly — no calculation required.
Rendering runs synchronous footing checks at the moment a statement is produced. These guard rails confirm the arithmetic holds right now:
Assets = Liabilities + EquityNet Income = Revenue − Expenses- The cash-flow statement foots (operating + investing + financing reconciles to the change in cash)
Guard-rail results surface in the validation block of a RenderingLite payload (passed, checks, failures, warnings).
Do not conflate guard rails with rule-engine verification. Guard rails are render-time footing checks — "does this statement balance right now." Rule-engine verification (verificationResults / verificationSummary on the envelope) is a separate, persisted audit-corpus outcome. Both can assert Assets = L + E, but they answer different questions and run at different times.
You do not create a Balance Sheet. You create a Report, and the statements are surfaced as Views of its facts.
The reports table is the package container. A Report row carries:
- Identity and period:
name,period_start,period_end,periods(JSON),taxonomy_id - Two independent lifecycles:
-
Generation —
generation_statusmovespending → generating → complete → published -
Filing —
filing_statusmovesdraft → under_review → filed → archived, withfiled_at/filed_by
-
Generation —
- A restatement chain via
supersedes_id/superseded_by_id, so a corrected report links to the one it replaces - Sharing provenance:
source_graph_id,source_report_id,shared_at
Package membership is implicit in fact dual-stamping: a Report owns FactSets (fact_sets.report_id), and FactSets own Facts (facts.fact_set_id). That dual stamp is the membership — there is no separate join table listing which statements belong to a report. The statement display order is fixed: Balance Sheet (1), Income Statement (2), Cash Flow (3), Equity (4), then Schedules (100).
This is why a statement is a view, not a stored artifact: the Report holds facts; the envelope renders them on demand.
There are several addressable surfaces, depending on whether you want a saved Report, a live ad-hoc statement, or a pivot grid. All authenticated curl examples read the API key from .local/config.json (created by just demo-user or any demo command) and target http://localhost:8000. Replace $GRAPH_ID with your tenant graph id, or use sec for the shared SEC repository.
Full request/response schemas live in the live OpenAPI spec — this page shows usage, not the full endpoint surface.
build-fact-grid produces a pivot table over the XBRL hypercube in the graph. It works on the SEC shared repo and on materialized tenant graphs. The request must supply elements and/or canonical_concepts, and at least one of periods / period_type / fiscal_year — otherwise it returns a 400.
curl -X POST "http://localhost:8000/extensions/roboledger/sec/operations/build-fact-grid" \
-H "X-API-Key: $(jq -r .api_key .local/config.json)" \
-H "Content-Type: application/json" \
-d '{
"elements": ["us-gaap:Assets"],
"entities": ["NVDA"],
"form": "10-K",
"period_type": "annual"
}'A richer pivot puts entities on rows and periods on columns, with custom column labels:
curl -X POST "http://localhost:8000/extensions/roboledger/sec/operations/build-fact-grid" \
-H "X-API-Key: $(jq -r .api_key .local/config.json)" \
-H "Content-Type: application/json" \
-d '{
"canonical_concepts": ["revenue"],
"entities": ["NVDA", "AMD"],
"form": "10-K",
"view_config": {
"rows": [{"type": "entity"}],
"columns": [{"type": "period",
"member_labels": {"2023-12-31": "FY23", "2024-12-31": "FY24"}}],
"values": "numeric_value",
"aggregation_function": "sum"
}
}'view_config.rows and view_config.columns each take axis configs whose type is one of element, period, dimension, or entity, with optional member_order, member_labels, and selected_members. The SEC graph this runs against is the one set up in the SEC XBRL Pipeline guide.
create-report renders the Balance Sheet, Income Statement, Cash Flow, and Equity facts for a tenant ledger and publishes them as a Report. taxonomy_id defaults to rs-gaap, but mapping_id is required — without a chart-of-accounts → GAAP mapping there is nothing to roll up.
curl -X POST "http://localhost:8000/extensions/roboledger/$GRAPH_ID/operations/create-report" \
-H "X-API-Key: $(jq -r .api_key .local/config.json)" \
-H "Idempotency-Key: $(date +%s)" \
-H "Content-Type: application/json" \
-d '{
"name": "Q1 2026 Financials",
"taxonomy_id": "rs-gaap",
"mapping_id": "<your mapping id>",
"period_start": "2026-01-01",
"period_end": "2026-03-31",
"period_type": "quarterly",
"comparative": true
}'Obtain a real mapping_id from the auto-map-elements output or by creating a mapping association — see RoboLedger Operations. The operation returns an OperationEnvelope and accepts an Idempotency-Key header so retries are safe.
The rendered View is exposed on the Information Block envelope. GraphQL is served at POST /extensions/{graph_id}/graphql — the graph is the URL scope, so queries do not take a graphId argument.
curl -X POST "http://localhost:8000/extensions/$GRAPH_ID/graphql" \
-H "X-API-Key: $(jq -r .api_key .local/config.json)" \
-H "Content-Type: application/json" \
-d '{"query": "{ informationBlock(id: \"<structure_id>\") { blockType displayName view { rendering { rows { elementName depth isSubtotal values } periods { label start end } validation { passed checks failures } } } verificationSummary { passed failed } } }"}'informationBlock(id) returns the latest FactSet — the live closing-book view. To pin a specific frozen snapshot, query reportPackage(reportId), which rehydrates a saved Report as a package of rendered envelopes with each member pinned to its own FactSet.
Live versus pinned reads differ.
informationBlock(id)always reflects current facts;reportPackage(reportId)reflects the facts as they were when the report was created. The snapshot unit is the FactSet, not the structure.
live-financial-statement renders straight off the OLTP ledger with no saved Report. It is tenant-only — rejected on shared-repository graphs like sec. Supported statement_type values are income_statement, balance_sheet, cash_flow_statement, and equity_statement.
curl -X POST "http://localhost:8000/extensions/roboledger/$GRAPH_ID/operations/live-financial-statement" \
-H "X-API-Key: $(jq -r .api_key .local/config.json)" \
-H "Content-Type: application/json" \
-d '{"statement_type": "income_statement", "period_type": "annual", "fiscal_year": 2026}'The cash-flow path on this surface needs at least two periods so the indirect-method deltas can be computed. For comparative graph-hypercube analysis on the SEC repo, use financial-statement-analysis instead.
The same operations are exposed as MCP tools — build-fact-grid, financial-statement-analysis, and live-financial-statement. On a shared SEC repository graph, financial-statement-analysis takes a ticker and statement_type to pull a company's statement straight from the graph hypercube. They delegate to the same operations layer as the REST surface, so an AI assistant gets identical numbers. See the SEC XBRL Pipeline guide for MCP client setup.
The dashboard's BlockView is an envelope-driven dispatcher. It receives an Information Block envelope and switches on block_type and the requested View mode to choose how to display it — rendered statement rows, a fact table, the model structure tree, and so on. Because the envelope already carries pre-computed view.rendering rows plus the atom lists for the other five Views, the component dispatches and displays; it does not calculate. Switching View mode for a block is a client-side re-projection, not a new server request.
The reporting layer reads from three kinds of authored content, each with its own write path. Contributors extend the system by writing to the appropriate one rather than touching the renderer.
-
Information Blocks — the data envelopes the renderer surfaces. You do not author a statement block directly; you create a Report (
create-report) whose facts the envelope renders. Authored blocks such as Schedules are the place to originate intent. See Information Blocks. -
Taxonomy — the frameworks (
rs-gaap,fac) that define the calc DAG: which concepts exist, how leaves roll up into subtotals, and which presentation networks a Reporting Style can walk. Edits to a framework'staxonomy.jsonldflow into graphs through a library reseed. -
Event / ledger — the transactions, entries, and line items that become facts. The flow tag (
flow_element_id) on a line item is what makes flow-as-fact cash flow work.
A worked example — request a rendered Income Statement after you have a mapped ledger:
API_KEY=$(jq -r .api_key .local/config.json)
GRAPH_ID=<your tenant graph id>
# 1. Render live off the ledger to confirm the mapping foots
curl -X POST "http://localhost:8000/extensions/roboledger/$GRAPH_ID/operations/live-financial-statement" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"statement_type": "income_statement", "period_type": "annual", "fiscal_year": 2026}'
# 2. Once it foots, save it as a Report package
curl -X POST "http://localhost:8000/extensions/roboledger/$GRAPH_ID/operations/create-report" \
-H "X-API-Key: $API_KEY" \
-H "Idempotency-Key: $(date +%s)" \
-H "Content-Type: application/json" \
-d '{"name": "FY2026 Income Statement", "mapping_id": "<your mapping id>",
"period_start": "2026-01-01", "period_end": "2026-12-31",
"period_type": "annual", "comparative": true}'If a number is wrong, the fix is almost never in the renderer — it is in the mapping (a CoA account pointed at a subtotal instead of a leaf), the taxonomy (a missing calc arc), or the ledger (an untagged flow line). To explore the running surface end-to-end, just demo-roboledger provisions a tenant with synthetic data and a mapped chart of accounts.
Wiki Guides:
- Information Blocks - The data envelope, block types, FactSets, and the six View projections at the data layer
-
RoboLedger Operations - The command writes (
create-report,regenerate-report, period close) that produce the facts this page renders - Serialization & Export - Exporting and distributing rendered reports (iXBRL, package sharing)
-
SEC XBRL Pipeline - Load and query the SEC graph that
build-fact-gridandfinancial-statement-analysisrun against - Architecture Overview - How the reporting layer fits the broader platform
Codebase Documentation:
- Operations - Business-logic kernel; the operations the renderer delegates to
-
GraphQL - The Strawberry extensions surface that serves
informationBlockandreportPackage - API Documentation - API reference with machine-readable OpenAPI spec
© 2026 RFS LLC
- Authentication & API Keys
- Graphs & Multi-Tenancy
- Shared Repositories
- Graph Operations
- Querying the Analytical Graph
- Credits & Billing
- AI Operators & MCP
- Pipeline Guide
- Extensions Surface Overview
- GraphQL Reads
- RoboLedger Operations
- RoboInvestor Operations
- Connecting QuickBooks Locally