-
Notifications
You must be signed in to change notification settings - Fork 10
GraphQL Reads
This guide shows you how to run ad-hoc, strongly-typed reads against the operational (OLTP) extensions data of a graph using GraphQL. Every read goes through a single endpoint — POST /extensions/{graph_id}/graphql — that is scoped to one graph by its URL. It covers the endpoint and its auth model, how the schema is composed per deployment, worked curl queries against fiscalCalendar and entity, schema discovery through introspection, and the get-graphql-schema / query-graphql MCP tools that give an AI agent the same surface.
Quick Start: With an API key and a graph_id in hand, curl -X POST "https://api.robosystems.ai/extensions/$GRAPH_ID/graphql" with -H "X-API-Key: $ROBOSYSTEMS_API_KEY" and -d '{"query": "{ entity { id name } }"}' returns the parent entity for that graph.
Running your own stack? Every example here works against a local deployment: use
http://localhost:8000and the key fromjust demo-user. See Local Development.
RoboSystems exposes a graph's data through two distinct read planes. This page is about the first one. Knowing which plane you want keeps you from reaching for the wrong tool.
| Plane | Endpoint / tool | Backs onto | Reads | Use when |
|---|---|---|---|---|
| GraphQL / OLTP (this page) |
POST /extensions/{graph_id}/graphql · MCP query-graphql
|
The per-tenant PostgreSQL extensions database | The live operational source of truth — ledger and investor records as they stand right now | You want "what's in the books right now": entity metadata, fiscal calendar state, agents, transactions, mappings, period-close status |
| Cypher / OLAP |
POST /v1/graphs/{graph_id}/query/cypher · MCP read-graph-cypher
|
The materialized LadybugDB graph | The analytical projection, blue/green materialized from the OLTP database | You want an analytical scan over the materialized graph — multi-hop traversals, aggregate rollups, report rendering |
The split is deliberate. GraphQL reads hit PostgreSQL directly, so they always reflect the current operational state. Cypher reads hit LadybugDB, which is rebuilt from the OLTP data on a materialization cadence, so it is optimized for analytical scans but lags the OLTP database by one materialization cycle. GraphQL = the operational plane; Cypher = the analytical plane.
- The Two Read Planes
- Overview
- Prerequisites
- Quick Start
- The Endpoint
- Authentication
- Schema-per-Flag Composition
- A Worked Query: Fiscal Calendar and Entity
- More Example Queries
- Discovering the Schema
- From an AI Agent: the MCP Tools
- Error Surface
- Troubleshooting
- Self-hosted deployments
- Related Documentation
- Support
The GraphQL surface gives you typed, ad-hoc reads over a graph's operational data. Four ideas make it work end-to-end:
-
One endpoint, scoped by URL. All queries go to
POST /extensions/{graph_id}/graphql. Thegraph_idlives in the path, so a query is implicitly scoped to that one graph. You never passgraphIdas a query argument — that "wrong graph" failure mode is designed out. Auth and per-graph access are checked before any resolver runs. - Typed reads from shared Pydantic models. The schema is built with Strawberry and auto-derived from the same Pydantic response models that the REST write operations return. REST writes and GraphQL reads share one schema by construction, so the shapes never drift.
- Schema-per-flag composition. The schema you see depends on which extensions are enabled on the deployment. A ledger-only deployment has no investor fields at all — they are absent from introspection, not a runtime error.
- Thin resolvers, ops layer is the truth. Resolvers open an extensions-database session and delegate to the operations layer — the same functions the MCP tools and REST endpoints call. There is no business logic in the GraphQL layer itself.
- A RoboSystems account and an API key, created in the app under Settings → API keys. See Quick Start.
- A
graph_idto query against — list yours withGET /v1/graphsor copy it from the app's graph selector. A RoboLedger graph comes from roboledger.ai when you connect QuickBooks, or from Create Graph in the app. - On robosystems.ai the GraphQL endpoint is on, with both the ledger and investor field groups. A self-hosted deployment mounts it only when at least one extension (
ROBOLEDGER_ENABLEDorROBOINVESTOR_ENABLED) is enabled, behind theEXTENSIONS_GRAPHQL_ENABLEDkill switch.
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
# Run your first GraphQL read
curl -X POST "https://api.robosystems.ai/extensions/$GRAPH_ID/graphql" \
-H "X-API-Key: $ROBOSYSTEMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "{ entity { id name } }"}'POST /extensions/{graph_id}/graphql
-
graph_idis a FastAPI path parameter validated against the platform's graph-or-subgraph ID pattern. Three kinds of value work: an entity graph (kg+ 16 or more hex chars), a shared-repository id (sec), or the reservedlibrarysentinel, which browses the shared public taxonomy library rather than a tenant. It is not a GraphQL argument — resolvers read it from request context. -
Subgraph IDs match the pattern but are rejected with HTTP 403. A subgraph is a modality container with no extensions schema of its own, so
POST /extensions/{parent}_{name}/graphqlfails at runtime rather than at validation. Target the parent graph. - The request body is a standard GraphQL POST payload: a JSON object with a
querystring, optionalvariablesobject, and optionaloperationName. - The in-browser GraphiQL explorer (a GET on the same URL) is development-only and is not mounted on
api.robosystems.ai. Introspection over POST works on the hosted API, with or without credentials — see Discovering the Schema. - The endpoint is mounted only on deployments where
ROBOLEDGER_ENABLEDorROBOINVESTOR_ENABLEDis set, and a deployment can disable it entirely withEXTENSIONS_GRAPHQL_ENABLED=false. On robosystems.ai it is enabled.
This page shows usage examples. Every query the schema serves is documented field by field at robosystems.ai/docs/extensions/graphql; the endpoint's own request and response contract is in the API reference at robosystems.ai/docs/api. How the surface is built is in the codebase (graphql/README.md).
GraphQL reads use the same authentication as the rest of the API: the X-API-Key header.
curl -X POST "https://api.robosystems.ai/extensions/$GRAPH_ID/graphql" \
-H "X-API-Key: $ROBOSYSTEMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "{ entity { id name } }"}'Rules and behaviors:
- Create the key in the app under Settings → API keys; it is shown once, and can be scoped to all your graphs or to one graph. Send it in the header — never put it in the URL.
-
Authorization: Bearer(JWT) is a frontend concern only — don't use it for backendcurltesting. -
Introspection works without credentials; data does not. You can fetch the schema (an introspection query over POST) without an API key. A query for real data with no valid credentials returns HTTP 200 with a GraphQL error whose
extensions.codeisUNAUTHENTICATED— not a transport-level 401. Invalid or expired credentials do produce an HTTP 401. - Access is checked per graph before any resolver runs. A valid key that lacks access to the requested
graph_idreturns aFORBIDDENerror.
The schema is not static — it is composed at startup from the extensions enabled on the deployment. This means the set of available fields differs between a ledger-only deployment and a full ledger + investor deployment.
| Field group | Gated by | Examples |
|---|---|---|
| Ledger fields | ROBOLEDGER_ENABLED |
entity, agents, transactions, fiscalCalendar, periodCloseStatus, trialBalance, reports
|
| Investor fields | ROBOINVESTOR_ENABLED |
portfolios, securities, positions, holdings, portfolioBlock
|
| Always-on fields | not flag-gated |
informationBlock / informationBlocks, taxonomy blocks, the library* family, hello (an auth probe) |
The consequence: on a ledger-only deployment, a field like portfolios is absent from the schema entirely. Introspection won't list it, and querying it is a schema validation error rather than a runtime "not initialized" error. Clients should branch on the schema shape they discover through introspection rather than trial-and-error against fields that may not exist. This is why the recommended first step from any client — human or agent — is to read the schema (an introspection query or get-graphql-schema) and query only the fields the deployment actually exposes.
The always-on group is larger than one table cell suggests. Three query classes — information blocks, taxonomy blocks, and the library — are composed on every deployment regardless of flags, because they are cross-domain and not gated by a per-graph extension:
-
Information blocks —
informationBlock,informationBlocks -
Taxonomy blocks —
taxonomyBlock,taxonomyBlocks -
Library taxonomies —
libraryTaxonomies,libraryTaxonomy,libraryTaxonomyArcs,libraryTaxonomyArcCount -
Library elements —
libraryElements,libraryElement,searchLibraryElements,libraryElementTree,libraryElementEquivalents,libraryElementArcs,libraryElementClassifications -
Library structures —
libraryStructures,libraryStructure -
Liveness —
hello
The library* family needs no flag because its visibility follows the session search_path, which follows from the URL's graph_id: a tenant graph sees its own taxonomy rows with public fallback, while the reserved library sentinel browses the shared public library on its own.
curl -X POST "https://api.robosystems.ai/extensions/library/graphql" \
-H "X-API-Key: $ROBOSYSTEMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "{ libraryTaxonomies { name } }"}'The fiscalCalendar and entity reads are good first queries because they return a small, stable shape and exist on any RoboLedger-enabled graph.
export GRAPH_ID=kg... # from GET /v1/graphs or the app's graph selector
curl -X POST "https://api.robosystems.ai/extensions/$GRAPH_ID/graphql" \
-H "X-API-Key: $ROBOSYSTEMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "{ fiscalCalendar { closedThrough closeTarget gapPeriods closeableNow blockers } }"}'A response looks like this:
{
"data": {
"fiscalCalendar": {
"closedThrough": "2025-11",
"closeTarget": "2025-12",
"gapPeriods": 1,
"closeableNow": true,
"blockers": []
}
}
}The wire field names are the camelCase form of the underlying Pydantic fields: closed_through becomes closedThrough, close_target becomes closeTarget, gap_periods becomes gapPeriods, and so on. Querying the snake_case names fails. The full fiscalCalendar shape carries more than the five fields above — fiscalYearStartMonth, catchUpSequence, blocker-detail fields like pendingObligationCount and earliestPendingPeriod, lastCloseAt, lastSyncAt, and a periods list — discover them through introspection.
curl -X POST "https://api.robosystems.ai/extensions/$GRAPH_ID/graphql" \
-H "X-API-Key: $ROBOSYSTEMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "{ entity { id name legalName entityType fiscalYearEnd source } }"}'A response looks like this:
{
"data": {
"entity": {
"id": "...",
"name": "Cascade Advisory Group LLC",
"legalName": "Cascade Advisory Group, LLC",
"entityType": "corporation",
"fiscalYearEnd": "12-31",
"source": "native"
}
}
}Here too, legalName, entityType, and fiscalYearEnd are the camelCase of legal_name, entity_type, and fiscal_year_end. The source field reports where the entity's data originated (native, sec, quickbooks, xero, or plaid). The full entity shape also exposes identifiers (cik, ticker, exchange, sic, lei, taxId), status, isParent, parentEntityId, address fields, and timestamps.
GraphQL arguments are supported — the one thing you never pass is graphId. List resolvers take filtering and pagination arguments:
curl -X POST "https://api.robosystems.ai/extensions/$GRAPH_ID/graphql" \
-H "X-API-Key: $ROBOSYSTEMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "{ agents(agentType: \"customer\", limit: 10, offset: 0) { id name } }"}'The agents resolver accepts agentType, source, isActive, limit, and offset. Pagination is bounded: limit must be between 1 and 1000 and offset must be 0 or greater, otherwise the query returns an INVALID_PAGINATION error.
The full field list is published at robosystems.ai/docs/extensions/graphql — one page per query, with its arguments, its return type and every field of that type, and an example call. It is generated from the schema the API is actually serving, so it is deployment-accurate and cannot drift from the code. Every query field carries a description, which means introspection answers the same question offline: { __schema { queryType { fields { name description } } } }.
This page does not repeat that list. What follows is the orientation the generated reference cannot give you: which read to reach for, given a question.
| You want to know | Start with |
|---|---|
| What is blocking the month-end close |
fiscalCalendar — closedThrough, blockers, reconcilingItemCount, syncStaleDays in one read |
| Which schedules are drafted, posted or pending for a period |
periodCloseStatus, then periodDrafts for the entries themselves |
| Who owes us, and how concentrated it is |
agents(agentType: "customer") with openReceivable on each — names and balances together. openReceivablesByAgent gives the same balances keyed by id only |
| What a counterparty has done |
agent, then the event and transaction history behind the balance |
| The chart of accounts, and what is still unmapped |
accounts / accountTree, then unmappedElements and mappingCoverage
|
| Balances rolled up to reporting concepts |
mappedTrialBalance, or accountRollups for the CoA-level view |
| A published report, whole |
reportPackage — metadata plus every rendered block in one round trip |
| A single statement out of a report | statement(reportId:, blockType:) |
| A rendered schedule, rollforward or statement block |
informationBlock; pass scenarioId for a forecast slice and series for the whole period series |
| The books' scale and sync freshness | summary |
| What the taxonomy says about a concept |
libraryElement / searchLibraryElements, then libraryElementTree and libraryElementClassifications
|
Balances are in minor currency units. openBalanceCents is cents. Convert once, on the way in.
reportDownloadUrl(reportId, format, expiresIn) is worth calling out on its own: it is the only way to download a published report's serialization bundle. A download is a read of stored state, so it lives here rather than as a REST resource. Every format resolves to a short-lived presigned S3 URL — JSON-LD is stamped at publish time, XBRL is materialized and cached on first request — and the resolver returns that URL, never the bytes. expiresIn is bounded between 60 and 3600 seconds; out of range returns INVALID_EXPIRES_IN. A report with no published bundle raises REPORT_BUNDLE_NOT_AVAILABLE; an unknown id returns null.
On an investor-enabled deployment the investor query root adds portfolios, securities, security, positions, position, holdings and portfolioBlock. On any deployment, the taxonomy library reads (library*, searchLibraryElements) are always present — browse them with the library graph id.
On the hosted API you discover what fields a deployment exposes with an introspection query over POST (an AI agent uses get-graphql-schema, below). The in-browser GraphiQL explorer is development-only — it is not mounted on api.robosystems.ai; see Self-hosted deployments.
Introspection is a normal GraphQL query and works without credentials:
curl -X POST "https://api.robosystems.ai/extensions/$GRAPH_ID/graphql" \
-H "Content-Type: application/json" \
-d '{"query": "{ __schema { queryType { fields { name } } } }"}'This returns every top-level query field for that deployment — the authoritative answer to "what can I read here?". Ask for description alongside name and you get each field's documentation with it, which is the same text the published reference renders:
curl -X POST "https://api.robosystems.ai/extensions/$GRAPH_ID/graphql" \
-H "Content-Type: application/json" \
-d '{"query": "{ __schema { queryType { fields { name description } } } }"}'An AI agent reaches the same surface through two MCP tools on the RoboSystems MCP server. The graph_id comes from the agent's active workspace context, so — exactly as with the HTTP endpoint — it is never passed as a query argument.
| Tool | Purpose |
|---|---|
get-graphql-schema |
Returns the GraphQL schema. Default format: "sdl" returns the SDL text; format: "introspection" returns the full JSON introspection result. |
query-graphql |
Executes a read-only GraphQL query. Arguments: query (required), variables (optional object), operationName (optional). |
The intended flow is two steps: discover the schema, then query it.
1. get-graphql-schema # returns SDL; discover types and fields
2. query-graphql query="{ fiscalCalendar { closedThrough closeTarget } }"
query-graphql is strictly read-only. It rejects mutations and subscriptions before execution, and enforces a complexity gate: maximum query depth 10, maximum 200 fields, and maximum 20 aliases. Queries that exceed these limits are rejected rather than executed.
Direct HTTP queries are bounded too, just more loosely, and on a different third axis. The endpoint applies Strawberry limiters for maximum depth 15, maximum 30 aliases, and maximum 2000 tokens — deployment settings EXTENSIONS_GRAPHQL_MAX_DEPTH, EXTENSIONS_GRAPHQL_MAX_ALIASES, and EXTENSIONS_GRAPHQL_MAX_TOKENS, which a deployment's operator can tune at runtime through SSM without a redeploy. They exist to bound query cost against the small extensions OLTP connection pool, where each resolved field can open a session. Introspection is exempt from the depth limiter, so SDK codegen is unaffected.
Note the easy-to-confuse pairing: get-graphql-schema returns the GraphQL SDL for this OLTP plane, while get-graph-schema (no ql) returns the Cypher/graph schema for the analytical LadybugDB plane. They are different tools for different planes. See AI Operators and MCP for the full MCP tool surface.
GraphQL data errors return HTTP 200 with a typed code in extensions.code, so clients branch on the code rather than the HTTP status. (Invalid or expired credentials are the exception — those produce a transport-level HTTP 401.)
| Code | Meaning |
|---|---|
UNAUTHENTICATED |
No valid credentials |
FORBIDDEN |
Valid credentials, but no access to this graph |
INVALID_PAGINATION |
limit or offset out of range (limit 1–1000, offset ≥ 0) |
LEDGER_NOT_INITIALIZED |
Graph has no ledger schema yet — connect a data source or run a sync first |
INVESTOR_NOT_INITIALIZED |
Same, for the investor surface |
EXTENSION_NOT_PROVISIONED |
Graph isn't provisioned for the extension you queried |
Introspection works without credentials, but data resolvers require an API key. If schema browsing succeeds but every data query returns UNAUTHENTICATED, you're missing the X-API-Key header. Add it:
-H "X-API-Key: $ROBOSYSTEMS_API_KEY"If you are sending a key and get a transport-level HTTP 401 (not a GraphQL UNAUTHENTICATED error), the credential itself is invalid or expired — create a new key under Settings → API keys in the app.
The field you asked for isn't in this deployment's schema. Two common causes:
-
Wrong casing. Wire field names are camelCase.
close_targetis wrong;closeTargetis correct. -
Flag-gated field on a deployment that doesn't enable it. Investor fields like
portfoliossimply don't exist on a ledger-only deployment. Run an introspection query and query only fields the deployment actually exposes.
List the graphs your key can reach and take the graph_id of the one you want, or copy it from the app's graph selector:
curl "https://api.robosystems.ai/v1/graphs" -H "X-API-Key: $ROBOSYSTEMS_API_KEY"The graph exists but has no ledger schema yet. Connect a data source and run a sync — for example, connect QuickBooks from roboledger.ai. Once data is loaded, the ledger fields resolve.
You queried ledger fields against a graph that doesn't have the roboledger extension provisioned. Provision the extension on that graph, or query a graph that has it. (Shared-repository graphs such as the SEC repo deliberately declare the roboledger extension, so ledger-shaped reads work against shared data.)
limit must be between 1 and 1000 and offset must be 0 or greater. A limit of 0 or a negative offset trips this. Set them within range.
The graph is scoped by the URL path, never by a query argument. The query body is just { entity { ... } } — there is no graphId field on any resolver. Put the graph in the URL: POST /extensions/$GRAPH_ID/graphql.
GraphiQL. On a development deployment, open the endpoint URL in a browser — http://localhost:8000/extensions/<your graph id>/graphql. GraphiQL renders with introspection enabled; use the Docs / Schema panel to browse types and fields and the editor to compose and run queries interactively.
A demo graph. just demo-roboledger provisions a graph with synthetic books (see RoboLedger Demo Walkthrough); on a fresh graph it is an alternative to connecting a data source. Demo scripts write the graph_id to .local/config.json under graphs.<slot>.graph_id — not a top-level key — and slot names vary by script (cascade_demo, cascade_demo_<entity_type>, roboledger_skeleton, roboinvestor_demo, saas_startup, custom_graph_demo, and others). Picking a slot positionally (to_entries[0]) often grabs another demo's graph, so list the slots and name yours:
jq -r '.graphs | keys[]' .local/config.json
GRAPH_ID=$(jq -r '.graphs.cascade_demo.graph_id' .local/config.json) # substitute your slotFor real synced data on your own stack, see Connecting QuickBooks Locally.
Wiki Guides:
-
Extensions Surface Overview - The three sub-surfaces (GraphQL typed reads, command writes, analytical view operations), the shared
OperationEnvelope, and the feature-flag story -
RoboLedger Operations - The write counterpart at
/extensions/roboledger/{graph_id}/operations/*, making the read/write split concrete -
Querying the Analytical Graph - The Cypher/OLAP plane this page contrasts against, with
read-graph-cypherandget-graph-schema -
AI Operators and MCP - The MCP tool surface, including
get-graphql-schemaandquery-graphql - Connecting QuickBooks Locally - Shows the OLTP → materialize → Cypher flow this page's analytical plane contrasts against
- RoboLedger Demo Walkthrough - Provisions a graph with synthetic books to run these queries against
Codebase Documentation:
- GraphQL Surface - Strawberry GraphQL surface, Pydantic auto-derivation, resolver patterns
- Operations - Business logic kernel the resolvers delegate to
- GraphQL reference - Every query field, with its arguments, return type and an example call, generated from the live schema
- API reference - API reference with machine-readable OpenAPI spec
Published at robosystems.ai/docs/technical · © 2026 RFS LLC
- Authentication & API Keys
- Operations Contract
- Errors & Rate Limits
- Versioning & Compatibility
- Graphs & Multi-Tenancy
- Graph Operations
- Querying the Analytical Graph
- File Uploads
- Credits & Billing
- Building Custom Integrations
- Build a Ledger Integration
- Extensions Surface Overview
- GraphQL Reads
- RoboLedger Operations
- QuickBooks Sync & Write Policy
- Chart of Accounts Mapping
- Period Close
- Forecasting & Metrics
- RoboInvestor Operations
- Information Blocks
- Information Block Reference
- Event-Driven Ledger
- Event Block Reference
- Taxonomy & Frameworks
- Reporting & Rendering
- Serialization & Export