Skip to content

GraphQL Channel

Query IdentityScribe via GraphQL with dynamic schema generation, cursor pagination, and an interactive GraphiQL IDE.

The GraphQL channel provides:

  • Dynamic schema generated from your entry type configuration
  • Collection and lookup queries with filtering, sorting, and Relay-style pagination
  • Change history API — global feed and per-entry history with temporal lookup
  • GraphiQL IDE — interactive query builder with schema explorer
  • Automatic Persisted Queries (APQ) — reduce bandwidth with query caching

Related:

Enable the GraphQL channel:

Terminal window
SCRIBE_GRAPHQL_ENABLED=true

Access points:

URLDescription
/graphqlQuery endpoint (POST) and GraphiQL UI (GET)
/graphql.sdlDownload schema in SDL format (application/graphql)
/graphql.jsonDownload schema as JSON introspection
/graphql/schema.graphqlAlias for /graphql.sdl
/graphql/schema.jsonAlias for /graphql.json

Use GraphiQL at /graphql to explore the schema interactively. The UI provides:

  • Schema browser with type definitions and field descriptions
  • Query autocompletion and validation
  • Query history and variables editor
  • Documentation for all types, fields, and arguments

For programmatic access, download the schema via /graphql.sdl (or /graphql/schema.graphql) or /graphql.json. See API Discovery for programmatic channel and schema discovery.

Fetch entries with filtering, sorting, and pagination:

query {
users(filter: "department=Engineering", first: 25, sort: "-modifyTimestamp") {
nodes {
entryDN
cn
mail
}
pageInfo {
hasNextPage
endCursor
}
}
}

Arguments: filter, sort, first/after, last/before, limit

Notes:

  • limit mirrors REST semantics and resolves to first or last based on cursor direction
  • limit is mutually exclusive with first/last; last requires before

Look up a single entry by ID, UUID, UOID, or DN:

query {
user(id: "dXNlcjo5aXg") {
entryDN
cn
mail
memberOf
}
}

Look up any entry type via the node query:

query {
node(id: "dXNlcjo5aXg") {
... on User {
cn
mail
}
... on Group {
cn
member
}
}
}

All lookup queries accept GlobalId, UUID, UOID, or DN. GlobalId is fastest since the type is encoded. See Entry identifiers for details.

Filter syntax matches the REST API — FleX, JSON, SCIM, or LDAP:

# FleX syntax
users(filter: "department=Engineering AND active=true")
# LDAP syntax
users(filter: "(&(department=Engineering)(active=TRUE))")

See Filters Reference for complete syntax.

Relay-style cursor pagination with first/after (forward) or last/before (backward). limit is a REST-style alias that resolves based on cursor direction.

# First page
query {
users(first: 25) {
nodes { cn }
pageInfo {
hasNextPage
endCursor
}
}
}
# Next page
query {
users(first: 25, after: "Y3Vyc29yOjI1") {
nodes { cn }
pageInfo { hasNextPage endCursor }
}
}

Connections expose nodes, edges, pageInfo, and count.

  • Select nodes or edges (not both)
  • count(mode: ESTIMATED) is fast and approximate (default)
  • count(mode: EXACT) is precise but slower
  • For count-only queries, request only count
query {
users(filter: "department=Engineering", first: 25) {
count(mode: ESTIMATED)
pageInfo { hasNextPage endCursor }
nodes { cn }
}
}

Change connections (changes) expose the same count field and modes.

Operational timestamp fields accept a format argument:

  • Fields: createTimestamp, modifyTimestamp, verifiedTimestamp
  • Presets: ISO (default), ISO_DATE, ISO_TIME, RFC_1123, UNIX, UNIX_MS, LDAP
  • Custom DateTimeFormatter patterns are also supported
query {
user(id: "dXNlcjo5aXg") {
modifyTimestamp(format: "UNIX_MS")
}
}

Query change history via the global changes feed or per-entry changes field.

View an entry at a specific point in time using the at argument:

query {
user(id: "dXNlcjo5aXg", at: "2024-01-15T10:00:00Z") {
cn
mail
department
}
}

The at argument accepts:

  • Cursor: Event cursor from a previous change (e.g., AAFntW9XAAAAAAApQwAA)
  • ISO timestamp: 2024-01-15T10:00:00Z
  • Partial date: 2024, 2024-01
  • ISO duration: P30D, PT1H
  • HOCON duration: 30d, 2 weeks
  • Relative expressions: now, now-1h

Future timestamps are clamped to now.

Query changes across all entry types:

query {
changes(range: "24h", type: [MODIFY], first: 100) {
nodes {
id
type
timestamp
entryUOID
entryType
data {
... on ModifyData {
diff { attribute operation value }
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}

Arguments:

ArgumentDescription
rangeISO interval or relative range (1h, now-1h/now, yesterday, this week, last 3 days)
since / untilCursor or timestamp bounds (ISO timestamp, partial date, duration, yesterday, this week, last 3 days). until defaults to now
typeFilter by ADD, MODIFY, MOVE, DELETE
entryTypeFilter by entry type names
affectsFilter MODIFY events by changed attributes
sorttimestamp (oldest first) or -timestamp (newest first)

Each entry has a changes field for its history:

query {
user(id: "dXNlcjo5aXg") {
cn
changes(first: 10, sort: "-timestamp") {
nodes {
type
timestamp
data {
... on ModifyData {
diff { attribute operation value }
}
}
}
}
}
}

MODIFY events provide three equivalent representations (all delta semantics):

FormatUse Case
diffInternal format with attribute-level operations (SET, ADD, REMOVE)
patchJSON Patch-inspired with /attribute/- for array additions
mergeJSON Merge Patch-inspired with changed values only
data {
... on ModifyData {
diff { attribute operation value }
patch { operation path value }
merge { attribute values }
}
}

Use cursors for resumable synchronization:

# Initial sync
query {
changes(first: 1000, sort: "timestamp") {
nodes { id type entryUOID data { ... } }
pageInfo { hasNextPage endCursor }
}
}
# Resume from cursor
query {
changes(after: "AAFntW9XAAAAAAApQwAA", first: 1000, sort: "timestamp") {
nodes { ... }
pageInfo { ... }
}
}

APQ reduces request payload size by caching query strings server-side. After registering a query with its SHA256 hash, subsequent requests send only the hash.

  1. First request: Send query + hash → server caches and executes
  2. Subsequent requests: Send hash only → server retrieves from cache
  3. Cache miss: Server returns PersistedQueryNotFound → client retries with full query

Apollo Client and similar libraries handle this flow automatically.

See Configuration Reference for cache size and TTL settings.

Three layers protect against denial-of-service:

LayerProtection
Parser limitsReject oversized payloads before parsing
Depth limitsReject deeply nested queries
Complexity limitsReject queries selecting too many fields

Defaults are suitable for most deployments. See Configuration Reference for tuning.

GraphQL errors include rich metadata in extensions:

{
"errors": [{
"message": "Query exceeds maximum depth of 12",
"extensions": {
"id": "err-a1b2c3",
"timestamp": "2024-01-15T10:00:00Z",
"code": "GRAPHQL_QUERY_DEPTH_EXCEEDED",
"graphqlCode": "BAD_USER_INPUT",
"kind": "INVALID_ARGUMENT",
"status": 400
}
}]
}
FieldDescription
codeIdentityScribe-specific error code for precise troubleshooting
graphqlCodeStandard GraphQL error code for client compatibility
kindError classification (e.g., NOT_FOUND, INVALID_ARGUMENT)
statusHTTP status that would apply

Standard GraphQL codes:

graphqlCodeUsed For
BAD_USER_INPUTInvalid arguments, out of range, failed preconditions
FORBIDDENPermission denied
UNAUTHENTICATEDAuthentication required
NOT_FOUNDEntry not found
INTERNAL_SERVER_ERRORServer errors, timeouts, unimplemented features

Use graphqlCode for standard error handling, code for detailed troubleshooting and support.

GraphQL operations emit OpenTelemetry traces:

SpanDescription
GRAPHQL.SearchCollection queries
GRAPHQL.LookupSingle entry lookups
GRAPHQL.NodePolymorphic node queries
GRAPHQL.HistoryChange history queries

Spans include operation name, query complexity, and error status.

Related: