Introspection API
August 27, 2026 ยท View on GitHub
Arranger exposes a set of read-only REST endpoints that describe the server's configuration and field structure at runtime. These endpoints are intended for tooling, LLM integration, and operator use: they do not require a GraphQL client and return plain JSON.
GET /introspection
Returns a summary of all catalogues registered on this server instance, along with their document types, GraphQL and introspection paths, and the path to the shared SQON schema.
{
"catalogCount": 2,
"mode": "multiple",
"status": "degraded",
"sqonSchemaPath": "/introspection/sqon",
"catalogs": {
"participants": {
"description": "Clinical trial participant records.",
"documentType": "participant",
"paths": {
"graphql": "/participants/graphql",
"introspection": "/introspection/participants"
},
"status": "available"
},
"biosamples": {
"documentType": "biosample",
"error": {
"code": "index_not_found",
"message": "The configured search index could not be found."
},
"paths": {
"graphql": "/biosamples/graphql",
"introspection": "/introspection/biosamples"
},
"status": "failed"
}
}
}
modeis"single"when one catalogue is registered,"multi"otherwise.descriptionis omitted when not set in the catalogue'sbase.json.paths.fieldsis only present in single-catalogue mode as a convenience alias (see below).- The top-level
statusis an aggregate over every registered catalogue:"healthy"(none failed),"degraded"(some failed, at least one available), or"unhealthy"(all failed). This is the same computation the server's readiness endpoint uses to decide whether to accept traffic (not yet documented on this page, see the health-checks tech-debt item). - Each entry under
catalogsalso has its ownstatus, scoped to that one catalogue:"available"or"failed". This is a different value set from the top-levelstatusabove; the two share a key name but describe different things. - A catalogue is never silently dropped from this list just because it failed to load.
error(an object with a machine-readablecode:index_not_found,permission_denied,connection_error,mapping_fetch_error,schema_build_error, orunknown_error, plus a short, safe-to-displaymessage) is only present on a catalogue entry when itsstatusis"failed", omitted entirely otherwise, not set tonull.
GET /introspection/:catalogueId
Returns field-level details for one catalogue: all fields, their Elasticsearch types, and which SQON operators apply to each type. Use this to discover what fields are queryable and how to construct valid SQON filters against them.
{
"catalogId": "participants",
"description": "Clinical trial participant records.",
"documentType": "participant",
"generatedAt": "2026-05-27T00:00:00.000Z",
"meta": { "authFiltered": false },
"operators": {
"keyword": ["in", "not-in", "some-not-in", "all", "filter"],
"long": ["in", "not-in", "gt", "gte", "lt", "lte", "between"],
"date": ["in", "not-in", "gt", "gte", "lt", "lte", "between"]
},
"fields": {
"participant_id": {
"displayName": "Participant ID",
"isArray": false,
"type": "keyword",
"unit": null
},
"age_at_diagnosis": {
"displayName": "Age at Diagnosis",
"isArray": false,
"type": "long",
"unit": "year"
},
"primary_diagnosis": {
"displayName": "Primary Diagnosis",
"isArray": true,
"type": "keyword",
"unit": null
},
"diagnosis_date": {
"displayName": "Diagnosis Date",
"isArray": null,
"type": "date",
"unit": null
}
}
}
operatorsgroups valid SQON operators by field type. To find which operators apply to a given field, look upoperators[field.type]. Only types actually present in the catalogue's index appear here.fieldslists every indexed field, each withdisplayName,isArray,type, andunit.isArrayandunitare always present: they arenullwhen nothing declared them, so an absent key means the server predates the field rather than the value being unset.fields[].isArraydeclares whether one document can hold more than one value for that field.truemeans it can,falsemeans a configuration declared it single-valued, andnullmeans nothing declared it either way. This cannot be inferred fromtype, because Elasticsearch never enforces cardinality against its mapping: any field can hold an array unless something says otherwise. Treatnullas undeclared rather than asfalse, and note that which reading is the cautious one depends on the operator. Anallclause needstrueto be satisfiable at all, whereas combining twoinclauses on one field is only safe when it isfalse.description, at the top level, is the catalogue's own description. It is omitted when not configured; field entries carry nodescriptionof their own.meta.authFilteredindicates whether a server-side filter was active when the response was generated (i.e. the field list may be narrowed by access control).
Note: In single-catalogue mode, /introspection/fields is an alias for this endpoint, and this disappears when a second catalogue is added. Code that hardcodes /introspection/fields should be updated to use the explicit catalogue ID path before adding a second catalogue.
:catalogueId also accepts a documentType
The path segment accepts either a catalogue's real catalogueId or its documentType, provided the documentType names exactly one catalogue on this server. A real catalogueId is always checked first and always wins, so this can never route to the wrong catalogue because of a naming collision.
documentType has no uniqueness guarantee (see Concepts), so a value shared by more than one catalogue returns 409 instead of silently resolving to one of them:
{
"documentType": "records",
"error": {
"code": "ambiguous_document_type",
"message": "documentType \"records\" matches multiple catalogues; use the catalogue id instead."
},
"matchingCatalogueIds": ["mutation", "correlation", "protein", "expression"]
}
/{catalogueId}/graphql accepts the same rules, in the leading path segment.
Failed catalogues: if a catalogue is registered but its search index couldn't be reached (see status in GET /introspection above), this endpoint still returns 200, with a minimal payload instead of the full field/operator listing:
{
"catalogueId": "biosamples",
"description": "Tissue sample records.",
"documentType": "biosample",
"status": "failed",
"error": {
"code": "index_not_found",
"message": "The configured search index could not be found."
}
}
documentType and (when configured) description come straight from the catalogue's base.json, same values GET /introspection would show for it, since that much is known from config alone, independent of whether the index is reachable. description is omitted when not set, same as everywhere else. Every other path for that catalogue (its GraphQL endpoint included) returns 404 instead, with the same fields plus a details pointer back to this endpoint, while it remains failed. This endpoint is the one place its status stays reachable without that extra pointer.
GET /introspection/sqon
Returns the SQON JSON Schema and operator metadata shared across all catalogues: combination operators (and, or, not), field operators with value types and applicability, and accepted aliases.
Use this to validate or describe SQON structure independently of any specific catalogue's field set. For field-specific operator applicability, use /introspection/:catalogueId instead.
See SQONs in detail for full documentation of the SQON filter language.
GraphQL introspection
The REST endpoints above are Arranger's own introspection API and are always available regardless of GraphQL settings. GraphQL's built-in introspection system (__schema, __type queries) is a separate capability that Arranger gates with the disableGraphQLIntrospection flag.
By default, GraphQL introspection is disabled when NODE_ENV=production and enabled otherwise. You can also control it explicitly:
- In a catalogue config file (
base.json):"disableGraphQLIntrospection": true - Via environment variable:
DISABLE_GRAPHQL_INTROSPECTION=true
Disabling GraphQL introspection is recommended in production to avoid exposing schema structure to clients through __schema/__type queries (OWASP A02).
Network aggregation caveat: When federated search is active, Arranger queries each remote node's GraphQL endpoint using __type to discover its aggregation field types at startup. If a remote node has GraphQL introspection disabled, that node's schema discovery fails and it is reported as an errored node with zero hits for the lifetime of the querying server's process. Until this dependency is replaced with a REST-based discovery call, do not set disableGraphQLIntrospection: true on any node that serves as a remote target in a federated deployment.