Skip to content

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:

  1. 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, ... }).
  2. 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 enforces maxCountInBatch, the error-taxonomy layer filters errors for omitErrorTag-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

Last updated · 501 distilled / 1,218 read