skip to content

Directive Definitions

Declaring a directive: its arguments, the locations it may appear at, and whether it repeats. Interviewers ask because directives are the schema's extension point and the part tooling sees least.

part ofGraphQLoverview, primer and where to startread it →
on this pageshow

questions

3

What does a directive definition in GraphQL SDL declare?

level: juniorimportance: must knowfreq 48%

answer

  1. Shape and placement, not behaviour
  2. Only one clause is mandatory
  3. Arguments follow field-argument rules
  4. One appearance per spot by default

basics

~20 s

A directive definition declares the directive's name, the arguments it accepts with their types and defaults, whether it may be applied more than once in the same place, and, mandatorily, the list of locations where it may be applied. It declares no behaviour.

solid answer

~50 s

In SDL a directive is declared as `directive @name(arg: Type = default) repeatable on LOCATION | LOCATION`. The name always carries the `@` and lives in the directive namespace, separate from type names. Arguments follow the same rules as field arguments: input types only, named rather than positional, optionally Non-Null, optionally with a default. The `on` clause is required and is the point of the whole declaration - it is an explicit list of grammar positions, and applying the directive anywhere else is a validation error rather than something silently ignored. `repeatable` is optional; without it the directive may appear at most once at any one location. Four directives are pre-declared in every schema and must not be redeclared: `@skip` and `@include` at executable locations, `@deprecated` and `@specifiedBy` at type-system locations. Nothing in the definition says what the directive does.

code

graphql · 12 lines
graphql
directive @sensorUnit(
  symbol: String!
  precision: Int = 2
) on FIELD_DEFINITION | SCALAR

directive @auditNote(text: String!) repeatable on OBJECT | FIELD_DEFINITION

type SoilProbe @auditNote(text: "Field set frozen 2026-04-11") {
  id: ID!
  moisturePercent: Float! @sensorUnit(symbol: "%")
  batteryMillivolts: Int! @sensorUnit(symbol: "mV", precision: 0)
}

go deeper

for a junior

Be ready to read a directive @name(...) on ... line aloud and name each part: the name with its @, the arguments, and the location list. Know that the location list is required and that the built-in directives are already declared for you.

for a middle

An interviewer expects the mechanics: arguments restricted to input types with optional defaults, named rather than positional, the uniqueness-per-location rule that repeatable lifts, and that applying a directive at a location outside its on clause is a validation error rather than a no-op.

for a senior

Show that you treat the definition as a contract with a consumer that lives outside the schema language. Be able to say why a valid definition can still do nothing, and how you would keep a directive's arguments stable once tooling and schema files depend on their names.

for a principal

Own the question of when a custom directive is the right extension point at all, versus modelling the same information as ordinary schema. Directives are cheap to add and expensive to retire, because every consumer of them is bespoke and invisible to standard tooling.

A directive definition is a declaration of **shape and placement**. It says what the directive is called, what arguments it accepts, whether it may appear more than once in the same spot, and — crucially — exactly which positions in the grammar it is allowed to appear at. It says nothing whatsoever about behaviour. ## The five parts ```graphql "Marks a numeric field as carrying a physical unit." directive @sensorUnit( symbol: String! precision: Int = 2 ) on FIELD_DEFINITION | SCALAR ``` Reading that top to bottom: 1. **A description** (optional). A string or block string immediately before the `directive` keyword. It is documentation, and introspection returns it. 2. **The name** (required), always written with a leading `@`. Directive names are their own namespace — introspection returns them in a `directives` list separate from `types`, so a directive called `@sensorUnit` does not collide with a type called `SensorUnit`. Two directives in one schema may not share a name. 3. **An argument list** (optional). Omit the parentheses entirely when the directive takes no arguments. 4. **The `repeatable` keyword** (optional), which sits between the argument list and `on`. 5. **The `on` clause** (required). A `|`-separated list of location names. A leading `|` is allowed, which is convenient when the list is long enough to wrap. ## Arguments follow the field-argument rules exactly A directive argument is declared like any other argument in the schema: a name, a type, and an optional default value. The type must be an **input type** — a scalar, an enum, an input object, or a list/Non-Null wrapper around one of those. An output object type, an interface or a union is a schema validity error. Arguments are named, never positional, so applications may supply them in any order and may omit any argument that is nullable or has a default. There is one rule specific to directives that is easy to trip over: a directive definition must not reference itself, directly or indirectly, through the types and default values in its own argument list. A directive whose argument is an input object that is itself annotated with that same directive is invalid, because the server would have no order in which to resolve it. ## `repeatable` and the uniqueness rule Without `repeatable`, a directive may appear **at most once at any given location**. Applying it twice to the same field definition is a validation error. Adding the keyword lifts that restriction, and every consumer of the directive must then read a *list* of applications rather than a single one. Introspection reports the flag as `isRepeatable` on the directive's entry. ## What the definition deliberately does not say Nothing in a directive definition describes what happens when the directive is applied. The specification assigns behaviour only to the handful of directives it defines itself; for anything you declare, the definition is a contract about where the annotation may be written and what data it carries, and something outside the schema language has to act on it. That is why a schema can be perfectly valid while a custom directive does nothing at all. ## The built-ins are already declared Every schema comes with the specification's own directives pre-declared: `@skip` and `@include` at executable locations, and `@deprecated` and `@specifiedBy` at type-system locations. You do not write definitions for them, and redeclaring one of those names is an error. ## A worked example A farm sensor graph tracks soil probes across paddocks. The schema author wants units and free-text audit annotations to travel with the schema: ```graphql directive @sensorUnit(symbol: String!, precision: Int = 2) on FIELD_DEFINITION directive @auditNote(text: String!) repeatable on OBJECT | FIELD_DEFINITION type SoilProbe @auditNote(text: "Field set frozen 2026-04-11") { id: ID! moisturePercent: Float! @sensorUnit(symbol: "%") batteryMillivolts: Int! @sensorUnit(symbol: "mV", precision: 0) } ``` `@sensorUnit` is not repeatable, so a second `@sensorUnit` on `moisturePercent` fails validation. `@auditNote` is, so `SoilProbe` may accumulate as many notes as the team writes. Applying `@sensorUnit` to `SoilProbe` itself fails too, because `OBJECT` is not in its `on` clause — and that failure is the entire value of the clause. The location list is the only part of the definition a server is obliged to enforce, and it is what turns an annotation from free-form text into something a schema can be checked against. In an interview, the answer that scores is the one that separates the three concerns cleanly: the **name and arguments** are the data the annotation carries, the **`on` clause** is where it may be written, and the **behaviour** lives somewhere else entirely.

  • Can a directive definition's argument be typed as an object type from the schema?
    No. A directive argument must be an input type - a scalar, an enum, an input object, or a list or Non-Null wrapper around one of those - exactly like any field argument. Typing one as an object type, an interface or a union is a schema validity error caught when the server builds the schema, not at request time.
  • What does the `repeatable` keyword change?
    By default a directive may appear at most once at any given location, and a second application is a validation error. `repeatable` lifts that, so the same directive may be applied several times in the same spot - and every consumer must then read a list of applications rather than one. Introspection reports the flag as `isRepeatable` on the directive's entry.
  • What happens if a document or schema applies a directive that was never defined?
    It fails validation. The rule is that every applied directive must be defined by the schema; an unknown directive name is rejected before execution, so a client sending one gets an error response with no data rather than having the annotation ignored. That is why you cannot invent an annotation ad hoc in a query.

A directive definition is a form's blank template: it fixes the field names, which answers are allowed, and which documents the form may be stapled to. It says nothing about who reads the form or what they do with it.

saying these in an interview costs you the question

  • Says the definition specifies what the directive does at runtime
  • Thinks the `on` location clause is optional
  • Assumes a defined directive may be applied anywhere
  • Believes every directive may be repeated at one location
  • Types a directive argument as an output object type
  • Thinks @deprecated must be declared by the schema author

context

open as a page

In GraphQL, how do a directive's executable locations differ from its type-system locations?

level: middleimportance: should knowfreq 40%

basics

~20 s

Executable 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.

open as a page

Why does GraphQL introspection not show where a custom directive is applied?

level: seniorimportance: nice to knowfreq 20%

basics

~20 s

Introspection describes the type system, so it lists a schema's directive definitions but never their applications. The only applications visible are two the meta-schema was given dedicated fields for: deprecated, as isDeprecated and deprecationReason, and specifiedBy, as specifiedByURL.

open as a page