In GraphQL, how do a directive's executable locations differ from its type-system locations?
answer
- Two different documents, two families
- Who writes it, and when it is read
- One is per request, one per schema build
- The `FIELD` versus `FIELD_DEFINITION` trap
basics
~20 sExecutable locations are positions in the operation document a client sends - a field selection, a fragment spread, an operation. Type-system locations are positions in the schema - an object type, a field definition, an enum value. A definition may list either family, or both.
solid answer
~40 sThe `on` clause of a directive definition names positions in one of two different documents. Executable locations - `QUERY`, `MUTATION`, `SUBSCRIPTION`, `FIELD`, `FRAGMENT_DEFINITION`, `FRAGMENT_SPREAD`, `INLINE_FRAGMENT`, `VARIABLE_DEFINITION` - are written by the caller, arrive with each request, and are evaluated during that request's execution. Type-system locations - `SCHEMA`, `SCALAR`, `OBJECT`, `FIELD_DEFINITION`, `ARGUMENT_DEFINITION`, `INTERFACE`, `UNION`, `ENUM`, `ENUM_VALUE`, `INPUT_OBJECT`, `INPUT_FIELD_DEFINITION` - are written by the schema author, are the same for every request, and are read once when the server builds the executable schema. The pair that catches people is `FIELD` versus `FIELD_DEFINITION`: one is a field selected in a document, the other a field declared in the schema, and a directive is valid at only whichever it lists. The specification's own executable directives are `@skip` and `@include`; its type-system ones are `@deprecated` and `@specifiedBy`.
code
graphql · 7 linesdirective @sensorUnit(symbol: String!) on FIELD_DEFINITION
directive @sampleAt(isoTimestamp: String!) on FIELD
type Paddock {
id: ID!
moisturePercent: Float! @sensorUnit(symbol: "%")
}go deeper
Know that a directive you see in a query and a directive you see in the schema file are not interchangeable, and that the definition's location list decides which one it is. Recognising @skip in a document and @deprecated in SDL is enough at this level.
Be able to name both families, explain who authors each and when it is read, and walk through why applying a FIELD_DEFINITION directive inside a query is a validation error. Knowing the specification defines only four directives is expected here.
Demonstrate the design judgement: a permanent property of a field belongs at a type-system location, because putting it at an executable location makes it optional for every caller and adds per-request parsing and validation work. Be ready to explain the cost difference concretely.
Own where the extension point should live across a whole graph. Executable directives are a public contract with every client and are nearly impossible to change once shipped; type-system directives are internal to the schema and cheaper to evolve, but only if the tooling that reads them is centralised.
GraphQL has two entirely separate documents in play, and a directive location names a position in one of them. **Executable locations** are positions in the operation document a client sends. **Type-system locations** are positions in the schema the server publishes. The `on` clause of a directive definition lists which of those positions the directive may be written at, and the two families are read by different machinery at different times. ## The executable locations These are the ones a client may type into a request: `QUERY`, `MUTATION`, `SUBSCRIPTION`, `FIELD`, `FRAGMENT_DEFINITION`, `FRAGMENT_SPREAD`, `INLINE_FRAGMENT`, `VARIABLE_DEFINITION`. A directive at one of these locations arrives with every request that uses it, is validated against the schema's directive definitions along with the rest of the document, and is evaluated during execution of that one request. Change the document, and the annotation changes. The specification defines exactly two executable directives, `@skip` and `@include`; everything else at these locations is an extension. ## The type-system locations These are positions in the schema itself: `SCHEMA`, `SCALAR`, `OBJECT`, `FIELD_DEFINITION`, `ARGUMENT_DEFINITION`, `INTERFACE`, `UNION`, `ENUM`, `ENUM_VALUE`, `INPUT_OBJECT`, `INPUT_FIELD_DEFINITION`. A directive at one of these is written by the schema author, not the caller. It is fixed for every request the schema serves, and a server reads it once, when it builds the executable schema out of the SDL. That is a real operational difference and not just a conceptual one: a type-system annotation costs nothing per request, which matters when the graph is held to something like a 340 ms p99 budget, whereas an executable directive is parsed, validated and evaluated on every single request that carries it. The specification defines two type-system directives, `@deprecated` and `@specifiedBy`. ## The trap: `FIELD` versus `FIELD_DEFINITION` This is the pair interviewers actually probe, because the English word "field" covers both. `FIELD_DEFINITION` is a field **declared** on an object or interface type in the schema — the `moisturePercent: Float!` line. `FIELD` is a field **selected** in an operation document — the `moisturePercent` a client writes inside a selection set. A directive defined `on FIELD_DEFINITION` cannot be written in a query, and a directive defined `on FIELD` cannot be written in the SDL. Both attempts are validation errors, not silently-ignored annotations. The same split shows up elsewhere. `ARGUMENT_DEFINITION` is the declaration of an argument in the schema; there is no executable location for an argument written in a document, because an argument in a document is not a position a directive can attach to. `INPUT_FIELD_DEFINITION` is a field of an input object type in the schema. `SCHEMA` is the schema definition block itself. ## Can one definition list both? Yes — the `on` clause is just a list of location names, and nothing forbids mixing families: ```graphql directive @traceTag(label: String!) on FIELD | FIELD_DEFINITION ``` That is legal, but it is usually a modelling smell: the same name is now read by two different consumers at two different times, one of them per request and one of them at build time, and the code that acts on it has to be written twice. None of the specification's own directives mix families, and a candidate who notices that is showing the right instinct. ## A worked example In a farm sensor graph, the unit a numeric field carries is a property of the schema, and the sample window a caller wants is a property of the request: ```graphql directive @sensorUnit(symbol: String!) on FIELD_DEFINITION directive @sampleAt(isoTimestamp: String!) on FIELD type Paddock { id: ID! moisturePercent: Float! @sensorUnit(symbol: "%") } ``` ```graphql query PaddockMoisture { paddock(id: "PDK-27") { moisturePercent @sampleAt(isoTimestamp: "2026-09-03T04:15:00Z") } } ``` Swap those two annotations and both documents fail validation. That is the whole distinction: the location list is not decoration, it is the mechanism that decides whether an annotation belongs to the contract or to the call. ## Why interviewers care Getting this wrong produces a specific, common design error — reaching for an executable directive to express something that is a permanent property of a field, which pushes the burden onto every caller and makes the property something a client can simply decline to send. The answer that lands is: **type-system directives annotate the contract, executable directives annotate one call**, and the `on` clause is where you commit to which one you meant.
- What exactly is the difference between the FIELD and FIELD_DEFINITION locations?`FIELD_DEFINITION` is a field declared on an object or interface type in the schema - the `moisturePercent: Float!` line. `FIELD` is a field selected inside a selection set in an operation document. A directive declared only on `FIELD_DEFINITION` cannot be written in a query, and one declared only on `FIELD` cannot be written in the SDL; both misuses fail validation.
- May a single directive definition list both an executable and a type-system location?Yes - the `on` clause is just a list, and mixing families is legal. It is usually a smell, though: the same name is then read by two different consumers at two different times, once at schema build and once per request, so the logic behind it has to be written twice. None of the specification's own directives mix families.
- Which directives does the GraphQL specification itself define, and where?Four. `@skip` and `@include` are executable, written by clients in a document. `@deprecated` and `@specifiedBy` are type-system, written by the schema author. Everything else you meet - cost annotations, ownership directives, caching hints - is a server extension or a separate specification, not part of GraphQL's own type system.
A type-system directive is a note printed on the menu; an executable directive is a note the diner writes on the order slip. The kitchen reads the first once, when the menu is set, and the second every time an order arrives.
saying these in an interview costs you the question
- Thinks any defined directive can be written in a query
- Confuses FIELD with FIELD_DEFINITION
- Says a client can apply @deprecated in a document
- Believes type-system directives are evaluated per request
- Assumes the specification ships a large directive library