RoboLedger Operations
RoboLedger's command writes and analytical views are graph-scoped operations at POST /extensions/roboledger/{graph_id}/operations/{op_name}. Command writes mutate the ledger (update the entity, close a period, generate a report); analytical views read facts back out: from the XBRL hypercube, from a report read whole, or from the live OLTP ledger. Both share one response contract and live under the same URL prefix.
Quick Start: With an API key and a RoboLedger graph, curl any operation below against https://api.robosystems.ai, sending the key in the X-API-Key header.
Running your own stack? Every example here works against a local deployment: use
http://localhost:8000and the key fromjust demo-user. See Local Development.
Table of Contents
- Overview
- Prerequisites
- Quick Start
- The Operation Envelope Contract
- Command Writes
- Period Close
- QuickBooks Write-Back and Reconciling Items
- Forecasting and Metrics
- Analytical View Operations
- Worked Example: build-fact-grid
- Financial Statements
- Gotchas and Pitfalls
- Self-hosted deployments
- Related Documentation
Overview
RoboLedger's extensions surface follows a CQRS split with three sub-surfaces, all graph-scoped at the URL level:
| Sub-surface | Endpoint | Purpose |
|---|---|---|
| Typed reads | POST /extensions/{graph_id}/graphql | Strawberry GraphQL — entity, fiscal calendar, mappings, reports. See GraphQL Reads. |
| Command writes | POST /extensions/roboledger/{graph_id}/operations/{op_name} | Mutations — close a period, update the entity, generate a report. |
| Analytical views | POST /extensions/roboledger/{graph_id}/operations/{view_name} | Read-only queries over the XBRL hypercube (build-fact-grid, financial-statement-analysis), over a report read whole (disclosures, information-block), over an SEC filing's own text (describe-filing, search-text, read-text), and over the live OLTP ledger (live-financial-statement, consolidation-worksheet, preview-consolidation-entries). |
This page covers the latter two. The graph_id always comes from the URL path — FastAPI dependencies validate authentication and per-graph access before the handler runs. The reads sibling is documented in GraphQL Reads, and the surface as a whole in Extensions Surface Overview.
Command writes and analytical views share one response shape — the OperationEnvelope — and accept an Idempotency-Key header. Most operations complete synchronously; a few dispatch to a background worker and stream progress over Server-Sent Events.
Prerequisites
- A RoboSystems account and an API key, created in the app under Settings → API keys. See Quick Start.
- A graph with an initialized RoboLedger ledger. On the hosted platform a RoboLedger graph comes from roboledger.ai when you connect QuickBooks, or from Create Graph in the app.
- The SEC examples (
/extensions/roboledger/sec/...) need a subscription to the SEC repository, from Repositories in the app. ROBOLEDGER_ENABLEDgates the command-write operations. The analytical view router is gated independently byFACT_GRID_ENABLED, so deployments serving only the SEC shared repository can mountbuild-fact-gridwithout enabling RoboLedger tenants. On robosystems.ai both are on; a self-hosted deployment sets them in its environment.
Quick Start
Set your credentials once, then reuse them across calls:
export ROBOSYSTEMS_API_KEY=rfs... # Settings → API keys at robosystems.ai
export GRAPH_ID=kg... # from GET /v1/graphs or the app's graph selector
Every example below uses https://api.robosystems.ai and the X-API-Key header. The full endpoint and schema reference — request bodies, response models, every field — lives in the extensions API reference at https://robosystems.ai/docs/extensions. This page carries the concepts and the worked examples; it does not re-tabulate the endpoint surface.
The Operation Envelope Contract
Every command write and analytical view returns the same envelope. This uniformity is what lets a client treat all writes identically — parse one shape, branch on status, and follow up via operation_id if needed.
| Field | Wire alias | Meaning |
|---|---|---|
operation | operation | The kebab-case operation name (e.g. close-period). |
operation_id | operationId | An op_-prefixed ULID identifying this execution. |
status | status | completed, pending, or failed. |
result | result | The typed payload (operation-specific) or null. |
at | at | ISO-8601 UTC timestamp. |
created_by | createdBy | The acting user id. |
idempotent_replay | idempotentReplay | true when this is a cached replay, not a fresh execution. |
The envelope is defined in middleware/operations.py. Wire aliases are camelCase (operationId, createdBy, idempotentReplay), and the model is populate_by_name=True, so both spellings are accepted on input.
Idempotency-Key. Send an Idempotency-Key header to make a write safe to retry. A replay with the same key returns the cached envelope (with idempotentReplay: true) without re-executing. Reusing a key with a different request body returns 409 — the body fingerprint must match, so the cache never silently overwrites a different operation. The analytical views, apart from live-financial-statement and consolidation-worksheet, accept the header and ignore it: a read has nothing to deduplicate, so every call runs fresh and idempotentReplay is always false.
Synchronous vs async. Most operations run synchronously and return 200 with status: completed. A few — notably auto-map-elements — dispatch to the background worker, return 202 with status: pending, and stream progress. The operation_id bridges to the monitoring surface: subscribe to GET /v1/operations/{operation_id}/stream (SSE) to follow a long-running operation to completion.
Command Writes
Command writes are the mutation half of the surface. The pattern is uniform: POST to /extensions/roboledger/{graph_id}/operations/{op_name} with a JSON body, get back an OperationEnvelope. The handlers are thin — they validate, delegate to the operations kernel, and wrap the result in the envelope.
The registered command-write operations, grouped by workflow stage. They live in the routers/extensions/roboledger/operations/ package, one module per API-reference tag (setup, taxonomy, information_blocks, ledger, close, reports, distribution):
| Stage | Operations |
|---|---|
| Setup | initialize, initialize-chart-of-accounts, record-opening-balance, update-entity, change-reporting-style |
| Taxonomy Blocks | create-taxonomy-block, update-taxonomy-block, delete-taxonomy-block, link-entity-taxonomy |
| Mapping | create-mapping-association, delete-mapping-association, auto-map-elements |
| Information Blocks | create-information-block, update-information-block, delete-information-block, bind-text-block, evaluate-rules |
| Forecasting and Metrics | compute-forecast, backfill-plan-history, compute-metrics, assert-metrics |
| Agents | create-agent, update-agent |
| Management Dimensions | create-dimension, update-dimension |
| Event Blocks | create-event-block, update-event-block, execute-event-block, preview-event-block |
| Reconciling Items | preview-reconciling-item, resolve-reconciling-item |
| Reconciliations | preview-reconciliations, refresh-reconciliations, record-statement-balance, set-reconciliation-policy, sign-off-reconciliation |
| Event Handlers | create-event-handler, update-event-handler |
| Journal Entries | update-journal-entry, delete-journal-entry, propose-consolidation-entries, set-allocation-override, record-acquisition, record-ownership-change, record-deconsolidation |
| Schedules | promote-obligations, rebuild-schedule, terminate-schedule |
| Close Workflow | set-close-target, close-period, reopen-period |
| Reports | create-report, regenerate-report, delete-report, share-report, revoke-report-share, file-report, transition-filing-status |
| Publish Lists and Distribution | create-publish-list, update-publish-list, delete-publish-list, add-publish-list-members, remove-publish-list-member, block-source-graph, unblock-source-graph |
auto-map-elements is the async one (returns 202 pending); the rest complete synchronously. The exact request body for each is in the extensions API reference. The workflows with their own pages:
| Workflow | Operations | Page |
|---|---|---|
| Month-end close | set-close-target, promote-obligations, terminate-schedule, close-period, reopen-period | Period Close |
| Account reconciliations | preview-reconciliations, refresh-reconciliations, record-statement-balance, set-reconciliation-policy, sign-off-reconciliation | Period Close |
| QuickBooks write-back and drift | execute-event-block, preview-reconciling-item, resolve-reconciling-item | QuickBooks Sync and Write Policy |
| Mapping | create-mapping-association, delete-mapping-association, auto-map-elements, initialize-chart-of-accounts | Chart of Accounts Mapping |
| Planning and KPIs | compute-forecast, backfill-plan-history, compute-metrics, assert-metrics | Forecasting and Metrics |
| Events and journal entries | create-event-block, update-event-block, preview-event-block, update-journal-entry, delete-journal-entry | Event-Driven Ledger |
| Schedules and other blocks | create-information-block, update-information-block, evaluate-rules | Information Blocks |
Forecasting and metrics. A forecast is a scenario authored as a forecast Information Block. compute-forecast walks it forward month by month into scenario-stamped statement FactSets, halting at the first month that fails verification. backfill-plan-history stamps the historical months a scenario stands on. compute-metrics derives a metric block's ratios for a period, and assert-metrics records observed values into one. All four are deterministic and consume no credits. The block shape, the four drivers and a full scenario payload are on Forecasting and Metrics.
Opening balances. Books whose history does not come with them start from a balance sheet. record-opening-balance takes one balance per balance-sheet account, in cents, debit or credit in the account's normal direction, and posts them as one opening entry dated the day before the entity's first fiscal month. Every month's opening balances then include it and no month's activity does, so the first month's cash flow and statement of equity start from it rather than counting it. Any difference between debits and credits goes to the equity account named in equity_element_id (usually Opening Balance Equity), and an income or expense account is refused. It is one per entity: recording it again replaces it until the entity's first close, change-calendar-start moves it with the start, and it cannot be reversed. A live statement column that reaches back before the books begin is left out and named in omitted_periods.
One example, update-entity, illustrates the partial-update convention. Only the non-null fields in the body are applied; an empty body returns 400 "No fields provided for update."
curl -X POST "https://api.robosystems.ai/extensions/roboledger/$GRAPH_ID/operations/update-entity" \
-H "X-API-Key: $ROBOSYSTEMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"legal_name": "Cascade Advisory Group, LLC",
"tax_id": "12-3456789",
"state_of_incorporation": "WA",
"fiscal_year_end": "12-31",
"entity_type": "llc"
}'
Downloading a report bundle is a read, not an operation. Generating, regenerating, and filing reports are command writes (create-report, regenerate-report, file-report, transition-filing-status), but fetching the produced artifact is not: the former GET .../reports/{id}/download REST resource has been retired. Use the GraphQL field reportDownloadUrl(reportId:, format:, expiresIn:) on POST /extensions/{graph_id}/graphql, or the MCP tool get-report-bundle, which returns the same link. Every flavor — HOLON_JSONLD (the default, stamped at publish), TAVI, XBRL_2_1 (both built on first download) — resolves to a short-lived presigned S3 URL the client follows directly; the API never streams the bytes. The holon is the one flavor that carries the whole report, and omittedContent on the result names what the other two leave out. See GraphQL Reads.
Period Close
close-period locks one month: it checks the closeable gate, publishes eligible drafts to QuickBooks on a write-back graph, posts the rest, advances closed_through, stamps the month's canonical statement FactSets, and records a receipt, as one operation. Only the QuickBooks publish markers commit ahead of the rest, so a retried close never writes an entry to QuickBooks twice. Months close sequentially (period must be closed_through + 1) and only once they are over. A month that can't close returns 422 with a structured detail.blockers array (sequence_violation, period_incomplete, sync_stale, pending_obligations, stranded_obligations, reconciling_items, unposted_source_events, unreconciled_accounts, unmapped_accounts, and others). reopen-period unlocks only the most recently closed month and needs a reason.
curl -X POST "https://api.robosystems.ai/extensions/roboledger/$GRAPH_ID/operations/close-period" \
-H "X-API-Key: $ROBOSYSTEMS_API_KEY" \
-H "Idempotency-Key: close-2026-03-$(date +%s)" \
-H "Content-Type: application/json" \
-d '{"period": "2026-03"}'
The fiscal calendar, every blocker and its remedy, the account reconciliations the close waits on, the full result and receipt, the MCP async close, reopening, ending a schedule early and the closed-period write gate are on Period Close.
QuickBooks Write-Back and Reconciling Items
On a graph whose QuickBooks connection is qb_authoritative, RoboLedger-originated drafts are written to QuickBooks at close (the entries_published_to_qb / entries_posted_locally split in the close result), or one at a time with execute-event-block. A transaction edited in QuickBooks after it posted is flagged as a reconciling item rather than overwritten. preview-reconciling-item shows the difference, and resolve-reconciling-item settles it by restate, catch_up or acknowledge. Pass the preview's drift_detected_at back as expected_drift_detected_at, so a resolution decided on one preview is refused with 409 if the item was flagged again since. Sync windows, the write-back predicate, retry after a QuickBooks rejection, and disconnect vs. sever are on QuickBooks Sync and Write Policy.
Forecasting and Metrics
Scenarios, the four drivers, line values and line growth, the verification halt, plan-history backfill, and the metric operations are on Forecasting and Metrics.
Analytical View Operations
Analytical view operations are read-only. Two of them, build-fact-grid and financial-statement-analysis, query the XBRL hypercube — the Fact nodes materialized into the LadybugDB graph. Five more run xbrlkit's tools over a report or filing held whole and don't touch the graph, and live-financial-statement, consolidation-worksheet and preview-consolidation-entries read the OLTP ledger (below). They share the OperationEnvelope contract with command writes and live under the same /operations/ prefix, but they mutate nothing.
Two view operations read a report whole instead of querying the hypercube. disclosures returns the map of a report's sections, one row per note or statement family with its block ids. information-block reads one section in full: rows with values per period, breakdowns by the section's own axes, calculation footing and text blocks. Both run xbrlkit's own tools over the report: the published filing on the SEC repository, or the ledger's own report on a tenant graph (pass report_id; on SEC, ticker or report_id). Call disclosures first, since it is cheap. information-block is the expensive call, one block at a time.
Three view operations read an SEC filing's own text, and work on the SEC shared repository only. describe-filing maps one filing: entity, periods, statements and disclosures by role, axes, and its text sections (the Items of a 10-K or 10-Q, its largest text blocks), each with the character offset it starts at. search-text returns every place the filing's whole text says something, cover page, footnotes and exhibits included, with the section each match falls in. The query is words matched in order (| between alternative phrases, a trailing * for a stem), not a regular expression. read-text returns one window of that text from an offset, with next_offset while more follows. A ticker picks the latest annual report, narrowed by fiscal_year and period_type; accession picks one filing; form: "8-K" picks the latest earnings release with its exhibits. On a tenant graph all three return 400: a ledger files no document, and its report reads through disclosures and information-block.
build-fact-grid is the primary view operation. It takes a set of concept filters and period filters, walks the matching Fact nodes, deduplicates them, and returns them as a flat list alongside the aspects those facts span.
It does not pivot. Arranging the facts into a table is the consumer's job, because collapsing cells safely requires the full aspect signature — two facts can share an element and a period end and still differ on period_start, duration_type, unit, or entity. Summing across those is a presentation decision the endpoint deliberately refuses to make on the caller's behalf. The same logic is exposed as the MCP tool build-fact-grid, with the same flat contract.
Two things make this router distinct from the command writes:
- It is gated independently. The view router checks
FACT_GRID_ENABLED, notROBOLEDGER_ENABLED. A deployment that serves only the SEC shared repository can mount fact-grid without provisioning any RoboLedger tenant. - It reads the OLAP graph, not the OLTP database. Tenant data only appears in the hypercube after materialization — a freshly-written ledger will not surface in a fact grid until the blue/green materialization runs. The SEC shared repository is always materialized.
The request body is a CreateViewRequest (defined in models/api/views/view_config.py). The two filter axes you must satisfy:
- Concept filter —
elements[](XBRL qnames:rs-gaap:Assetson a ledger's graph,us-gaap:Assetson SEC) or, on the SEC repository,canonical_concepts[](cross-filer ids likerevenue, set on standard elements only, so a filer that reports the line under its own extension is missing). - Period filter —
periods[](YYYY-MM-DD), orperiod_type(annual/quarterly/instant), or, on the SEC repository,fiscal_year.
You must provide at least one concept filter and at least one period filter, or the request returns 400. Note that fiscal_period is not a period filter — it narrows the fiscal context of an already-scoped query.
On a shared-repository graph (SEC), entity or entities is required as well: those graphs host thousands of filers, so an unscoped query returns an arbitrary slice of arbitrary companies. A tenant graph is already scoped to its entity by the URL — and that entity is often a private company with no ticker or CIK to filter on — so the requirement applies only to shared repos.
Other optional fields scope the query further: entity, entities[], form, fiscal_period (FY, Q1, …), include_summary, limit (default 250, max 5000), and view_config.
canonical_concepts, form, fiscal_year and fiscal_period are written by the shared-repository pipeline, so they exist on the SEC repository only. A ledger's graph has no canonical concepts, forms or fiscal periods, and a tenant query that uses any of them returns 400 naming the field, rather than an empty grid that reads as "no data". On a ledger's graph, select by elements with periods or period_type.
Worked Example: build-fact-grid
A balance-sheet rollup against a tenant graph, scoped to three concepts at one instant:
curl -X POST "https://api.robosystems.ai/extensions/roboledger/$GRAPH_ID/operations/build-fact-grid" \
-H "X-API-Key: $ROBOSYSTEMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"elements": ["rs-gaap:Assets", "rs-gaap:Liabilities", "rs-gaap:StockholdersEquity"],
"periods": ["2026-03-31"],
"period_type": "instant",
"include_summary": true
}'
The envelope's result is a ViewResponse — metadata, dimensions, facts, summary:
{
"metadata": {
"view_id": "3e0b2208-ab92-4629-9e74-e69a7a4b60e0",
"facts_processed": 3,
"construction_time_ms": 42.1,
"source": "fact_grid",
"period_start": null,
"period_end": null,
"truncated": false
},
"dimensions": [
{"name": "Element", "type": "element",
"members": ["rs-gaap:Assets", "rs-gaap:Liabilities", "rs-gaap:StockholdersEquity"]},
{"name": "Period", "type": "period", "members": ["2026-03-31"]}
],
"facts": [
{"element_id": "rs-gaap:Assets", "element_name": "Assets",
"period_start": null, "period_end": "2026-03-31", "duration_type": null,
"value": 1842300.0, "unit": "USD", "entity_ticker": null, "entity_name": null},
{"element_id": "rs-gaap:Liabilities", "element_name": "Liabilities",
"period_start": null, "period_end": "2026-03-31", "duration_type": null,
"value": 612450.0, "unit": "USD", "entity_ticker": null, "entity_name": null},
{"element_id": "rs-gaap:StockholdersEquity", "element_name": "StockholdersEquity",
"period_start": null, "period_end": "2026-03-31", "duration_type": null,
"value": 1229850.0, "unit": "USD", "entity_ticker": null, "entity_name": null}
],
"summary": {
"rs-gaap:Assets": {"count": 1, "total": 1842300.0, "average": 1842300.0,
"min": 1842300.0, "max": 1842300.0},
"rs-gaap:Liabilities": {"count": 1, "total": 612450.0, "average": 612450.0,
"min": 612450.0, "max": 612450.0},
"rs-gaap:StockholdersEquity": {"count": 1, "total": 1229850.0, "average": 1229850.0,
"min": 1229850.0, "max": 1229850.0}
}
}
facts[] is the payload: one deduplicated record per fact, carrying its full aspect signature (element_id, element_name, period_start, period_end, duration_type, value, unit, entity_ticker, entity_name). dimensions[] reports the aspects those facts span — element / period / entity with their observed members — which is what a client needs to lay out rows and columns itself. summary is present only when include_summary: true, and its total sums across every returned period, which is meaningful for duration facts and not for instants.
metadata.truncated is true when more facts matched than limit allowed. The returned facts are the most recent by period, so a truncated response is a valid answer to a narrower question — raise limit or tighten the filters to see the rest. A query that matches 10,000 or more facts before deduplication is refused with 400 instead, because the graph returns at most that many rows and anything built from them would be an arbitrary part of the match. Narrow it with fewer entities or elements, or a tighter period filter.
Deduplication keys on element, both period ends, entity and unit. A fact reported in two units, such as a figure and its translation into another currency, comes back as two records.
The view_config block scopes the query further; it is filtering only. This example asks the SEC shared repository for NVIDIA's annual revenue, restricted to three specific period members (NVIDIA's fiscal year ends in late January, so the members are its fiscal year ends, not calendar ones):
curl -X POST "https://api.robosystems.ai/extensions/roboledger/sec/operations/build-fact-grid" \
-H "X-API-Key: $ROBOSYSTEMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"elements": ["us-gaap:Revenues", "us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax"],
"entities": ["NVDA"],
"form": "10-K",
"period_type": "annual",
"view_config": {
"columns": [{"type": "period",
"selected_members": ["2023-01-29", "2024-01-28", "2025-01-26"]}]
}
}'
The result is again a flat list — three facts, one per fiscal year, each carrying the entity and the duration window it came from:
{
"facts": [
{"element_id": "us-gaap:Revenues", "element_name": "Revenues",
"period_start": "2024-01-29", "period_end": "2025-01-26",
"duration_type": "annual", "value": 130497000000.0, "unit": "USD",
"entity_ticker": "NVDA", "entity_name": "NVIDIA CORP"},
{"element_id": "us-gaap:Revenues", "element_name": "Revenues",
"period_start": "2023-01-30", "period_end": "2024-01-28",
"duration_type": "annual", "value": 60922000000.0, "unit": "USD",
"entity_ticker": "NVDA", "entity_name": "NVIDIA CORP"},
{"element_id": "us-gaap:Revenues", "element_name": "Revenues",
"period_start": "2022-01-31", "period_end": "2023-01-29",
"duration_type": "annual", "value": 26974000000.0, "unit": "USD",
"entity_ticker": "NVDA", "entity_name": "NVIDIA CORP"}
]
}
A ViewConfig has exactly two fields: rows[] and columns[], each a list of axis configs. An axis config has a type (element, period, or entity), an optional selected_members[], and include_null_dimension. rows and columns are naming conventions only — both filter identically, and neither controls layout. There is no values, aggregation_function, fill_value, or member_labels: ordering, labelling, and aggregation belong to the consumer. A dimension axis type is rejected outright, because the fact query filters on has_dimensions = false — dimensional facts never reach the response, so a dimension axis could only ever be a silent no-op. The full schema is in models/api/views/.
Financial Statements
Three operations render a complete statement rather than a free-form slice. Which one you want depends on where the truth for that graph lives.
live-financial-statement — tenant graphs
live-financial-statement is the authoritative path for a RoboLedger entity graph. It builds the statement directly from the tenant's OLTP ledger through the active CoA→GAAP mapping, so no materialization is required — a ledger you synced a minute ago is already reportable. It is rejected on shared-repository graphs.
It lives in a third router, reads.py, sibling to the operations/ package (command writes) and views.py (graph-backed views). All three mount under the same /operations/ prefix, but reads.py is gated by ROBOLEDGER_ENABLED — unlike views.py, which stays mounted for SEC-only deployments.
curl -X POST "https://api.robosystems.ai/extensions/roboledger/$GRAPH_ID/operations/live-financial-statement" \
-H "X-API-Key: $ROBOSYSTEMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"statement_type": "balance_sheet", "period_type": "annual", "fiscal_year": 2026}'
statement_type is one of income_statement, balance_sheet, cash_flow_statement, or equity_statement. Scope the window either explicitly (period_start / period_end) or by fiscal context (period_type, fiscal_year), anchored on the graph's fiscal calendar; explicit dates win. limit caps the fact rows returned. The result carries the resolved periods[] (current plus prior comparative), the rendered facts[] with depth and is_subtotal for indentation, and an unmapped_count.
entity_id names one entity's books; omitted, the statement is the group parent's. On the group parent, scope reads the group: combined sums every member consolidated in full, at the rs-gaap concepts and for the months each was a member, with nothing eliminated; consolidated adds the consolidation entity, whose entries are the eliminations and noncontrolling interest. A member on the equity method or at cost is never summed. combined_entity_ids on the response names the entities summed.
dimension slices an income statement by a management dimension: department, class, location, project, or an axis of your own. {"type": "department"} reads the current window with one column per member, then Unallocated (the lines no department tags), then Total, and dimension.ties_out says whether every row's members plus Unallocated equal its Total. {"type": "department", "member_id": "<id>"} reads one member's lines, current and prior, and member_id: "unallocated" the untagged ones. A line carries at most one member per axis, set through dimension_ids on a journal line and changed in place, posted or not, through update-journal-entry's line_dimensions. An event's own dimension_ids reach every line its handler writes, on the axes the line leaves open; a bank line takes classified_dimension_ids (or dimension_ids on a split part) beside its account; and a schedule's entry_template.dimension_ids tag every entry it drafts. The slice composes with scope; the consolidation entity's lines are untagged, so eliminations read as Unallocated. Any other statement returns 422, since its cash and equity lines carry no tag; the GraphQL trialBalance takes dimensionType and dimensionMemberId for balances.
include_sources: true says how each figure was made. Every row gains origin — accounts, subtotal (the rows beneath it) or computed (derived from other figures, or a flow) — and sources, one list per column of the accounts the figure was summed from: {entity_id, account_id, code, name, value}, largest first. A column lists its accounts only when they add up to the figure, so an equity line carrying the period's computed earnings, or a cash flow line, lists none rather than accounts it did not sum. On scope: consolidated each source names the entity whose books hold it — a member, or the consolidation entity for an elimination — and that pair opens the GraphQL accountRegister. It composes with a one-member dimension slice, and is refused (422) with a slice read by column, whose columns are members rather than periods. Off by default: it multiplies the response.
Membership is by whole month, so a window longer than a month counts each member for its own months: a balance sheet holds the members of the month it is struck in, an income statement sums each member's months of the window, and a cash flow or equity statement column a member joined or left within is left out and named in omitted_periods, since it would show the member's balances arriving or leaving with no line for them. A member acquired with an opening balance on the eve of the month it joined brings its cash as the group's cash acquired: in that month's column it is an investing inflow on Payments to acquire businesses, net of cash acquired, and the column opens on the group's own cash at the previous month's close. Tag the parent's payment to the sellers with that same row (flow_qname on its cash line) and the column shows the acquisition on one row, net of the cash that came with the member, with nothing left on the investment row.
The model behind the next operations — how an entity is treated, what the charts must carry, what the engine drafts and leaves for a person, the group close and a member's lifecycle — is Group Consolidation. The operations here are the reference for its request and response shapes.
consolidation-worksheet — tenant graphs
consolidation-worksheet reads the same ledger for one window with the entity as the column: the group parent, each member consolidated in full, their combined sum, the consolidation entity's adjustments, the consolidated total, and then each equity-method or cost member beside the sum (in_sum: false), never in it. Every column renders under the group parent's Reporting Style, and the two totals are the numbers live-financial-statement returns for its two group scopes. It is the read to check a consolidating entry against.
curl -X POST "https://api.robosystems.ai/extensions/roboledger/$GRAPH_ID/operations/consolidation-worksheet" \
-H "X-API-Key: $ROBOSYSTEMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"statement_type": "balance_sheet", "period_start": "2026-08-01", "period_end": "2026-08-31"}'
It takes the same statement_type and window fields. The result carries columns[] (each with its kind, entity_id, consolidation_method and ownership_pct), rows[] with one value per column in the Style's order, and validation for the consolidated column. A member outside its membership interval for the window has no column, and unmapped_entity_ids names any entity with no mapping yet. A window the live statement would leave out (before a summed entity's books begin, or a cash flow or equity statement a member joined or left within) is named in omitted_periods instead of totalled; when it is the worksheet's own window, rows is empty.
preview-consolidation-entries — tenant graphs
preview-consolidation-entries computes the consolidating entries a group's month needs from the books, and posts nothing. For each member consolidated in full it brings four adjustments to their month-end balance: the parent's investment eliminated against the member's capital (its equity lines other than closing entries, so contributions net of distributions), with the outside owners' share of that capital as noncontrolling interest; the balances the group's entities owe each other; the month's intercompany income against the matching expense; and the outside owners' share of the month's result. For each member on the equity method it adds the parent's share of the member's result.
curl -X POST "https://api.robosystems.ai/extensions/roboledger/$GRAPH_ID/operations/preview-consolidation-entries" \
-H "X-API-Key: $ROBOSYSTEMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"period": "2026-08"}'
The outside owners' share of a member's result is pro rata by default. Where they take a different share for a month — a preferred return, a waterfall, a special allocation — set-allocation-override records it on the member as an amount in cents or a percentage of the month's result, with the basis, and the engine proposes that instead, saying so in the entry's basis. A hand entry of nci_allocation would not do: the engine brings each kind back to its target.
curl -X POST "https://api.robosystems.ai/extensions/roboledger/$GRAPH_ID/operations/set-allocation-override" \
-H "X-API-Key: $ROBOSYSTEMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"entity_id": "ent_...", "period": "2026-08", "amount": 250000, "basis": "Preferred return to the outside member first"}'
The GraphQL allocationShare(entityId, period) reads a member's month as the engine will: its result, the outside owners' pro-rata share, the override if one is set, the share the entries use (effectiveAmount), and whether the consolidation entity has closed the month; it is null for an entity that is not a member consolidated in full that month. allocationOverrides(entityId) lists every month an override is set for.
A member the parent bought from its owners joins mid-life: joined_on is the day it joined, and its own books open on its balance sheet at acquisition, recorded with record-opening-balance. The parent paid the sellers, so its investment line names the seller, not the member (its cash line tagged Payments to acquire businesses, net of cash acquired, the row the group's cash flow nets the member's cash against), and until the acquisition is recorded the member's investment elimination is withheld as a residual with cause: pre_acquisition. record-acquisition records it: the consideration paid, in cents, and the basis, with optionally the fair value of the outside owners' interest (nci_fair_value, for goodwill on the whole business) and fair-value step-ups (allocations, each an identifiable asset or liability's concept and a nonzero amount; goodwill, the investment and noncontrolling interest are computed, never allocated). It writes a draft of kind acquisition in the consolidation entity, dated the join, which its close posts: the excess of the consideration over the parent's share of the member's equity at acquisition goes to the step-ups and the rest to goodwill, with the outside owners' share of what they add as noncontrolling interest. The engine then reads the consideration as the parent's investment in the member, and eliminates the rest as it does for any member. A bargain purchase is refused until confirm_bargain_purchase is set, after the reassessment ASC 805 asks for, and then recognized as a gain. Recording again replaces the draft; a posted acquisition is reversed first. A month whose recorded acquisitions paid more than the parent's investment account holds naming no member is incomplete, since the eliminations would credit an investment the parent never carried.
curl -X POST "https://api.robosystems.ai/extensions/roboledger/$GRAPH_ID/operations/record-acquisition" \
-H "X-API-Key: $ROBOSYSTEMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"entity_id": "ent_...", "consideration": 900000, "basis": "Purchase agreement, 1 July 2026"}'
When the parent later buys more of a member from its outside owners, or sells some of it to them while keeping control, record-ownership-change records it: the month the new share holds from, the share (ownership_pct, 50 or more; above the share before for a purchase, below it for a sale), the price in cents and the basis. It sets the member's share from that month and writes a draft of kind ownership_change in the consolidation entity, dated the month's first day, that its close posts. ASC 810 makes it an equity transaction, never a gain or loss: the investment elimination moves the share bought of the member's capital at the new share; the entry carries the price above that book value against the investment, moves the share bought of the member's results since it joined, and of any interest its acquisition gave the outside owners above their share of book value, out of noncontrolling interest, and takes the difference to the parent's equity (paid-in capital under a corporate Reporting Style, the capital account under an LLC or partnership one). Its lines carry the cash flow rows that move the price from investing to financing, the group's payment to its outside owners. A sale is the mirror: the parent's own entry, naming the buyer and not the member as the purchase named the seller, takes the proceeds against the carrying amount of the shares sold (its cost, pro rata — the response's carrying_released) and books its gain or loss on GainOrLossOnSaleOfStockInSubsidiary; the draft reverses that gain (gain_reversed), releases the parent's cost above book on those shares against the investment, credits the outside owners their larger share of the member's results, and takes the difference to the parent's equity — the group has no gain while control is kept — and its tags move the proceeds from the divestiture row to a financing inflow from the outside owners. The engine reads the price paid, net of what a sale released, as the parent's investment, and each member's share as of the month from these entries, so the months before keep the share they had. Changes are recorded in the order they happened; recording a month again replaces its draft. A change that moves the member across consolidation methods is not supported yet; changing ownership_pct with update-entity alone leaves the price unrecorded, and the month's investment elimination is withheld as a residual.
curl -X POST "https://api.robosystems.ai/extensions/roboledger/$GRAPH_ID/operations/record-ownership-change" \
-H "X-API-Key: $ROBOSYSTEMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"entity_id": "ent_...", "period": "2026-09", "ownership_pct": 80, "consideration": 150000, "basis": "Share purchase agreement"}'
A member leaves the group — sold, dissolved, control lost — at a month end: update-entity with left_on set to its last day, once its books are closed through that month, its bank feeds are disconnected, and its intercompany balances agree with the group's in that month (reconciling_items[] empty for it); each of the three refuses otherwise and names what to settle. An agreed balance is not in the way: it stays on the parent's books as a related-party balance once the member is a third party. From the month after, the group reads sum none of the member, its entity row reads is_former: true, and the month's proposals reverse its investment and intercompany eliminations on their own. What the consolidation entity still holds for it beyond those — goodwill and step-ups from its acquisition, the price above book of the parent's investment, the outside owners' accumulated share of its results, an associate's pickups — waits for record-deconsolidation: the consideration the parent received in cents (0 for a dissolution) and the basis. It writes a draft of kind deconsolidation in the consolidation entity, dated the first day of the month after the member's last, that its close posts: each of those balances is taken off, a member consolidated in full has the results it earned in the group credited back to the group's equity (the parent's close target; the consolidation template carries a row for every parent form, and a consolidation chart made before it did takes one through update-taxonomy-block and a mapping arc — the refusal names the concept) as they leave with its books, and the difference is the gain or loss on its leaving — the group's adjustment to the gain the parent's own sale entry took, so the consolidated gain is the consideration less the carrying amount of what left (ASC 810). The parent's own entry (cash against its investment, its own gain or loss) stays on its books. In that month the group's cash flow opens on the previous month's close and carries the member's cash on the divestiture row, net of cash divested, so it moves to a close without the member. Recording again replaces the draft; a posted deconsolidation is reversed first. A retained interest — the parent keeping a share and moving the member to the equity method — is not supported yet.
curl -X POST "https://api.robosystems.ai/extensions/roboledger/$GRAPH_ID/operations/record-deconsolidation" \
-H "X-API-Key: $ROBOSYSTEMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"entity_id": "ent_...", "consideration": 800000, "basis": "Sale agreement, 31 October 2026"}'
Accounts are found by the rs-gaap concept they map to, never by code, and the consolidation entity posts each adjustment to its own account mapped to that concept. An intercompany line is one whose event names another of the group's entities as its counterparty, through that entity's self agent; create-entity adds that agent for each operating entity it creates, and for the group parent. Each proposal is the difference between the target and what the consolidation entity already holds for that adjustment_kind and member, so an entry a person posted with the same kind counts toward it. up_to_date is true when the consolidation entity holds what the month needs, posted or in the month's own drafts, and nothing is withheld or left out.
A line that eliminates a cash flow carries the row it offsets in flow_qname: the parent's investment, the member's contributed capital and distributions, an intercompany loan, and the outside owners' share of capital, which is the group's own financing flow. Those lines are proposed apart from the untagged ones, so a tag never spreads to a line that offsets nothing. A row the consolidation entity's own cash flow statement does not present is left off, and notes[] says so.
propose-consolidation-entries takes the same period and writes the proposals as drafts in the consolidation entity, which its close posts. Proposing a month again replaces the drafts it wrote before and never touches a person's own entries; until the close posts them the month's own drafts are not held, so the preview still lists them, with their ids in drafted_entry_ids. Drafts of an earlier month are held, since that month's close will post them: proposing two open months in turn never eliminates twice. A month the consolidation entity has closed comes back closed, as posted, and nothing is computed or written for it.
On the consolidation entity's first month, what the members' history before it already needed (the investment eliminated against capital contributed earlier, a balance owed from before) is its opening balance, not that month's activity: the preview lists it as proposals with opening: true, dated the eve of the month, and proposing records it as one posted opening entry, replaced each time the month is proposed until the first close. The consolidated statements then open on it, and the outside owners' share opens at its balance instead of arriving with no row. With that opening the group's books start in its first month, so a consolidated column reaching back before it is left out and named in omitted_periods, as before any entity's books. The first month is the first open month of the consolidation entity's calendar, which has never closed; history that cannot open it (a pair withheld at the eve, or income the parent took before it) is caught up within the month and named in notes[].
An intercompany entry reversed on either side is matched by its original's counterparty, so the reversal nets it. A member past its left_on needs nothing from the engine, which reverses its cumulative eliminations; if the consolidation entity still holds an asset, liability or noncontrolling interest for it beyond those (the outside owners' share of its earlier results, an associate's pickups), the month is incomplete until record-deconsolidation takes off what notes[] lists. Once the group has closed a month, update-entity refuses an ownership change that would move a member to a different consolidation_method, since that would restate the months already closed; a change that keeps the method is accepted. The consolidation entity takes no joined_on or left_on: it belongs to every month it consolidates.
A difference between the parent's investment in a member and its share of the member's capital comes back in residuals[], and that member's investment elimination is withheld rather than plugged — unless the parent took the member's distributions as income, or the member was acquired (cause: pre_acquisition, explained by record-acquisition above). A parent line on OtherNonoperatingIncomeExpense, OtherNonoperatingIncome, InvestmentIncomeDividend or DividendIncomeOperating whose event names the member is a distribution taken as income; when those lines account for the difference, the income is reversed against the investment in the same entry and nothing is withheld. For a member on the equity method, the same income is reclassed against the investment in an equity_method_distribution entry each month. Intercompany balances or income the two sides disagree on come back in reconciling_items[], with the lower amount eliminated. An intercompany expense with no income against it, such as a cost one company paid and recharged to another at cost, stays in the consolidated statements and is listed in notes[].
A journal entry in the consolidation entity carries its adjustment_kind (elimination_investment, elimination_intercompany, nci_allocation, equity_method_pickup, manual, and the rest) and adjusts_entity_id, the entity whose figures it adjusts. Both are refused on an operating entity's books.
The consolidation entity's close is the group close. close-period with its entity_id closes the group's month only when every entity the month consolidates has closed it — the parent, each member inside its interval, and an equity-method member, whose result the pickup reads — the month's consolidating entries are posted or drafted, no residual or intercompany disagreement is left, and the consolidated balance sheet and equity statement pass their checks, read again once the close has posted the month's drafts, so a draft that would leave them not tying rolls the close back. Otherwise it is refused with entity_open, adjustments_unposted, residual_unexplained or consolidated_does_not_foot, and get-fiscal-calendar on the consolidation entity names what holds each in group_open_entities, group_pending_adjustments, group_unexplained and group_not_footing. allow_unexplained_residuals closes over a residual or disagreement and names each in the close's audit note; the difference stays in the consolidated statements. Once the group has closed a month, a member's month reopens only after the group's. The group close stamps the month's consolidated statements as the consolidation entity's own: an information-block or plan read with its entity_id returns the group's balance sheet, income statement, cash flow and equity statement as closed, in the parent's Style, and reopening the group's month takes them back.
financial-statement-analysis — materialized and shared-repository graphs
financial-statement-analysis renders the same four statement types out of the hypercube (Structure → FactSet → Fact). Use it against the SEC shared repository, or against a tenant graph whose ledger has already been materialized to LadybugDB.
curl -X POST "https://api.robosystems.ai/extensions/roboledger/$GRAPH_ID/operations/financial-statement-analysis" \
-H "X-API-Key: $ROBOSYSTEMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"statement_type": "balance_sheet", "report_id": "..."}'
The either/or is hard-enforced, and the two graph classes fail in opposite directions:
- On a tenant graph, omitting
report_idreturns400 "report_id is required for tenant graphs." - On a shared repository, omitting both
tickerandreport_idreturns400 "ticker is required on shared-repository graphs (e.g. SEC)."Supplyingtickerauto-resolves the latest matching filing; afiscal_yearthat resolves to nothing is a404, never a silent fallback to a different year.
How statements are rendered — the structures, the calc rollup, the footing — is the subject of Reporting and Rendering.
Gotchas and Pitfalls
Sequential Close Only
close-period requires period == closed_through + 1. Closing out of order returns 422 with blockers: ["sequence_violation"]. To advance multiple periods, close them one at a time in order. See Period Close.
"Ledger not initialized" 404
Any operation against a graph that has no extensions schema returns 404 "Ledger not initialized. Connect a data source first." Call initialize (or connect a data source) before any other RoboLedger operation.
update-entity With an Empty Body
update-entity applies only the non-null fields in the body. An empty body returns 400 "No fields provided for update."
close-period Blockers Are Structured
Do not parse a flat error string — read detail.blockers. sync_stale is overridable with allow_stale_sync: true only after you have manually verified the source data is current. A WRITE_BACK_FAILED failure carries a failed_events[] array for diagnosis, and retrying the close is safe. See Period Close.
Fact Grid Needs Both a Concept and a Period Filter
build-fact-grid enforces these guards, each a 400:
- No concept filter — omitting both
elementsandcanonical_concepts. - No period filter — omitting all of
periods,period_type, andfiscal_year.fiscal_periodalone does not satisfy this. - No entity filter on a shared-repository graph — omitting both
entityandentitiesagainst SEC returns"entity or entities is required on shared-repository graphs (e.g. SEC)."The same call against a tenant graph is fine, since the URL already scopes it. - An SEC-only selector on a ledger's graph —
canonical_concepts,form,fiscal_yearorfiscal_periodon a tenant graph is refused by name. Useelementswithperiodsorperiod_typethere. - Too broad — a query matching 10,000 or more facts before deduplication.
Fact Grid Only Sees Materialized Data
A tenant ledger must be materialized into LadybugDB before its facts appear in a grid. A ledger you just wrote will not surface until the blue/green materialization runs. The SEC shared repository is always materialized.
Posted Entries Are Immutable
update-journal-entry and delete-journal-entry operate on drafts only. To correct a posted entry, record a reversal via create-event-block with event_type='journal_entry_reversed'. Library-seeded mapping associations are likewise immutable — deleting one returns 403.
Idempotency-Key Reuse With a Changed Body
Reusing an Idempotency-Key with a different request body returns 409, not a silent overwrite. Use a fresh key per distinct operation.
Self-hosted deployments
On your own stack, just demo-roboledger provisions a tenant with 16 months of synthetic books in a few minutes and writes the API key and graph id into .local/config.json — see the RoboLedger Demo Walkthrough. To connect a real QuickBooks company to your own stack, see Connecting QuickBooks Locally. The write-policy model behind the entries_published_to_qb / entries_posted_locally split is on QuickBooks Sync and Write Policy.
Related Documentation
Guides:
- Extensions Surface Overview - The full RoboLedger / RoboInvestor extensions surface and CQRS split
- GraphQL Reads - The typed-read sibling surface (
POST /extensions/{graph_id}/graphql) - Reporting and Rendering - How statements are rendered and footed from the hypercube
- RoboLedger Demo Walkthrough - See the operations surface end-to-end against a populated tenant
- Period Close - Fiscal calendar, blockers, reconciliations, the close and its receipt, reopening
- QuickBooks Sync and Write Policy - Sync windows, write-back, reconciling items
- Chart of Accounts Mapping - Mapping model, auto-map, the closed-period mapping lock
- Forecasting and Metrics - Scenarios, drivers, plan history, metrics
API Reference:
- Extensions API reference - Every operation's request body and response model, generated from the live OpenAPI spec
- Analytical Views - The view operations, one page each
Codebase Documentation:
- RoboLedger operations package - The registered command-write operations, one module per tag
- Analytical view router -
build-fact-grid,financial-statement-analysis,disclosures,information-block,describe-filing,search-text,read-text - OLTP reads router -
live-financial-statement,consolidation-worksheet,preview-consolidation-entries - Operation envelope -
OperationEnvelope, idempotency, audit - Operations kernel - Business logic patterns behind every operation