CONCEPT Cited by 1 source
GraphQL query directive¶
Definition¶
A GraphQL query directive (spec term: executable
directive) is a directive whose declaration permits it on
one or more ExecutableDefinition locations — the parts of
a GraphQL document that the client writes, as opposed to
the schema the server publishes. Clients embed query
directives in their operations to express metadata that the
server should observe during execution.
The GraphQL spec enumerates 8 ExecutableDefinition locations:
| Location | Example |
|---|---|
QUERY |
query name @foo {} |
MUTATION |
mutation name @foo {} |
SUBSCRIPTION |
subscription name @foo {} |
FIELD |
query { product @foo {} } |
FRAGMENT_DEFINITION |
fragment x on Query @foo { } |
FRAGMENT_SPREAD |
query { ...x @foo } |
INLINE_FRAGMENT |
query { ... @foo { } } |
VARIABLE_DEFINITION |
query ($id: ID @foo) { } |
The spec's built-in query directives are @skip and
@include (both on FIELD | FRAGMENT_SPREAD |
INLINE_FRAGMENT) — conditional inclusion of parts of a
selection set based on a boolean variable.
Contrast with schema directives¶
The partner primitive schema directive targets TypeSystem locations (schema declarations). Differences:
| Aspect | Query directive | Schema directive |
|---|---|---|
| Written by | Client | Schema author |
| Carries | Client-side metadata | Server-side behaviour |
| Typical use | Observability tags, batching limits, error-taxonomy | Authorization, validation, resolution |
| Implementation | AST walk / extraction | Resolver wrapping |
Zalando's 2023 directive survey is explicit that the two kinds serve different purposes: "query directives are generally useful for clients to express certain types of metadata for the query. The schema directives are generally useful for declaratively specifying common server-side behaviors" (Source: sources/2023-10-18-zalando-understanding-graphql-directives-practical-use-cases-at-zalando).
Implementation idiom: extract, don't wrap¶
Unlike schema directives, the-guild's graphql-tools does
not expose a resolver-wrapping mechanism for query
directives — and Zalando's post explicitly endorses the
library's stance: "query directives are good for annotating
the query with metadata and not for resolver logic."
The canonical implementation is a two-phase AST walk on the parsed operation document:
- Pre-execution extract. Walk the operation AST,
collect the directive instances and their argument
values, emit a structured metadata object
(
{ tracingTag: "slo-1s", maxCountInBatch: 50, ... }). - Apply at execution-boundary. The extractor feeds
cross-cutting systems that sit around GraphQL
execution — the tracing layer stamps the span with
tracingTag, the batcher enforcesmaxCountInBatch, the error-taxonomy layer filters errors foromitErrorTag-annotated paths post-execution.
For @omitErrorTag specifically, Zalando uses a two-step
approach: pre-execution, collect the field-paths that
carry the directive; post-execution, intersect those paths
with the set of error paths GraphQL actually emitted and
omit matches from the span's error tag.
Zalando's query directives (worked example)¶
From the 2023 survey, a single product-card query exercises the full production query-directive footprint:
query product_card($id: ID!)
@component(name: "web-product-card")
@tracingTag(value: "slo-1s")
@maxCountInBatch(value: 50) {
product(id: $id) {
id
name
brand {
id
name
}
inWishlist @omitErrorTag
}
}
The four directives in use:
@component(name: String!) on QUERY— names the client component issuing the query. Used by field-lifecycle gates and by observability to attribute traffic.@tracingTag(value: String!) on QUERY | MUTATION | SUBSCRIPTION— stamps the OTel span with a client-defined tag for trace filtering.@maxCountInBatch(value: Int!) on QUERY— declares the maximum number of queries the client will batch together in a single HTTP request. Build-time-enforced via the persisted-queries DB (the server sees{ id, variables }; it trusts the build-time-verified batch cap).@omitErrorTag on FIELD— marks a field whose errors should not contribute to the span's overall error tag, sparing on-call rotations from alerting on known-noisy fields.
Why query directives are observability-shaped¶
The four production examples above are all observability /
governance concerns, not data-shape concerns (which is
what the built-in @skip / @include cover). This is not
coincidental:
- Query directives are carried by the operation, so they naturally encode per-operation policy (which tag, which SLO, which batch cap).
- Persisted queries make this especially powerful: the directive values are fixed at build time, the server trusts them, and the resulting production traffic is finite and labelled.
- Runtime behaviour that belongs to resolvers (authz, validation, resolution) lives in schema directives instead.
Seen in¶
- Zalando UBFF — the canonical production instance
covered in the 2023 directive survey. Production-observed
directives:
@component,@tracingTag,@omitErrorTag,@maxCountInBatch(Source: sources/2023-10-18-zalando-understanding-graphql-directives-practical-use-cases-at-zalando).
Related¶
- concepts/graphql-schema-directive — the TypeSystem partner primitive.
- concepts/graphql-persisted-queries — the substrate that makes build-time query-directive enforcement tractable.
- systems/graphql
- systems/zalando-graphql-ubff
- systems/opentelemetry — consumer of
@tracingTagand@omitErrorTag. - patterns/directive-based-field-lifecycle — schema-
directive lifecycle paired with
@componentquery directive.