Why is a custom executable directive in a GraphQL document not portable?
answer
- Count the directives the spec guarantees
- Two executable ones, and no more
- Unknown directive is a validation failure
- Introspection publishes shape, never behaviour
- Prefer an argument, which is typed and portable
basics
~20 sOnly @skip and @include are executable directives every GraphQL server must support. Anything else is defined by one server, with behaviour the specification never describes, so a document using it fails validation elsewhere as an unknown directive.
solid answer
~50 sThe GraphQL specification requires just two **executable** directives — `@skip` and `@include` — plus a couple of type-system ones such as `@deprecated` and `@specifiedBy`. Any other directive is something a particular schema chose to define, and the specification assigns it **no semantics at all**: what a server does when a document applies it is entirely that implementation's business. Two consequences follow. First, a document carrying `@currency(code: "EUR")` sent to a server whose schema does not define that directive is rejected in validation as an unknown directive — the whole request fails, not just that field. Second, even a server that *does* define it may do something different, because introspection publishes only a directive's name, description, arguments, locations and repeatability — never its behaviour. So no generic client can act on one, and support for applying custom executable directives during execution varies between server implementations.
code
graphql · 6 linesquery OrderTotals($orderId: ID!) {
order(id: $orderId) {
reference
totalCents @currency(code: "EUR")
}
}go deeper
Remember the short guaranteed list: @skip and @include are the executable directives every server has. If you see any other directive in a document, it belongs to that one schema and will not travel.
Explain both halves of the problem — an unknown directive fails validation outright, and even a known one has no specified meaning — and know that introspection publishes a directive's shape but never its behaviour.
Argue for the portable alternative: express client intent through arguments and variables, which are typed, validated and reflected in generated code, and keep custom directives on the schema side where build-time tooling reads them.
Decide when a custom schema-side directive is worth the tooling you must then own, and hold the line that documents clients send stay within the specified vocabulary so any server, mock or explorer can run them.
## What the specification actually mandates A conforming GraphQL schema carries a small set of built-in directives. On the **executable** side — the ones a client writes inside its document — there are exactly two: `@skip` and `@include`. On the **type-system** side — written in SDL, describing the schema — there are `@deprecated`, and `@specifiedBy` for pointing a custom scalar at its specification. That is the portable set. Everything beyond it exists because one schema author added it. This matters because "GraphQL supports custom directives" is true and misleading in the same breath. The specification supports *declaring* them: a schema may define a directive with a name, arguments and a location list, and clients may then apply it in those locations. What it does not do is say what applying one *means*. There is no execution rule attached to a custom directive anywhere in the specification. Semantics are implementation-defined, full stop. ## Failure mode one: the server has never heard of it A ticketing client sends a document that applies a directive its team invented for their own server: ```graphql query OrderTotals($orderId: ID!) { order(id: $orderId) { reference totalCents @currency(code: "EUR") } } ``` Against a server whose schema defines `@currency`, fine. Point the same document at any other GraphQL server — a staging environment built from an older schema, a partner's API, a public endpoint, a mock — and validation rejects it: ```json { "errors": [ { "message": "Unknown directive \"@currency\"." } ] } ``` This is a **request error**, so there is no `data` key and nothing in the document executes. A directive the target does not define is not silently ignored, and there is no "unknown directives are tolerated" mode in the specification. That is deliberate: silently dropping a directive whose whole purpose is to change behaviour would be worse than failing. Note how much stricter this is than `@skip`/`@include`, which are guaranteed present everywhere. A document restricted to the built-in executable directives runs against any conforming server; one custom directive binds it to a single schema. ## Failure mode two: the server has heard of it and means something else Suppose the target schema *does* define `@currency(code: String!) on FIELD`. Validation passes. Nothing guarantees you get the behaviour you expect, because nothing in the schema says what the behaviour is. One server might convert the amount; another might annotate the response; another might have defined it as a documentation marker that execution ignores entirely. The specification gives you no way to distinguish these. Introspection does not close the gap. A client can ask a server for its directives and get back, per directive, the **name**, an optional **description**, the **argument definitions**, the **locations** where it may be applied, and whether it is **repeatable**. Every one of those is structural. None of them is semantics. The description field is prose intended for a human reading documentation, not something a client can execute against. So a generic tool — an explorer, a typed client generator, a schema-diffing job — can tell you a custom directive exists and where it is legal, and can do precisely nothing useful with it. ## Failure mode three: the tooling on your own side Even within one organisation, custom executable directives leak. A typed client generator has no rule for what a custom directive does to a result type, so it generally ignores it — meaning generated types describe a shape the directive may have changed. Explorers will autocomplete it from introspection and give no hint about its effect. And support for even *reaching* a custom executable directive at execution time varies between server implementations: some expose a hook for it, some do not, and the ones that do disagree about ordering and about what a directive may modify. None of that variation is a bug in any of them, because there is nothing to conform to. ## What to do instead The portable way to express a per-request choice is the schema's own vocabulary, because that vocabulary *is* specified and *is* introspectable with types: * Want a currency? Make it an **argument**: `totalCents(currency: EUR)`, or expose a `totalIn(currency: Currency!): Money` field. The argument has a declared input type, it validates, it appears in generated code, and it works identically on every server. * Want a conditional? Use `@skip`/`@include`, which every server supports. * Want per-request metadata that is not really part of the graph — a trace id, a locale, a feature cohort? That belongs in a **transport header**, not in the document. The rule of thumb worth stating in an interview: a custom directive in an *executable document* couples that document to one server implementation, so keep them to schema-side annotations that tooling reads at build time, and express client intent through arguments and variables. And it is worth saying plainly which side of the line each claim sits on — that `@skip`/`@include` are specified, and that any behaviour attributed to a custom directive is a convention of one implementation.
- Is an unknown directive ignored by the server, or does it fail the request?It fails. A validation rule requires every directive used in a document to be defined in the schema, so an unknown one is a request error: the response carries `errors` and no `data`, and nothing executes. There is no lenient mode. Ignoring a directive whose purpose is to alter behaviour would silently give the client a different result from the one it asked for.
- What does introspection tell a client about a custom directive the server does define?Its name, an optional human-readable description, its argument definitions, the locations where it may be applied, and whether it is repeatable. All of that is structural. Nothing in introspection describes what applying the directive does, because the specification defines no semantics for custom directives — so a generic client can validate a use of it and still have no idea what it will cause.
- Are schema-side custom directives as problematic as executable ones?Much less so, because they are read at build time by tooling that was written for that schema, not sent across the wire by clients expecting a behaviour. Annotations that a code generator, linter or composition step consumes are a normal and useful pattern. The portability problem is specific to a *document* carrying a directive it needs the target server to understand at execution time.
A custom directive is a house abbreviation scribbled on an order. Your own kitchen knows it; hand the same slip to any other kitchen and it is not politely ignored, the order is refused.
saying these in an interview costs you the question
- Says an unknown directive is silently ignored
- Claims the spec defines behaviour for custom directives
- Thinks introspection describes what a directive does
- Counts @deprecated among the executable directives
- Assumes every server can execute custom directives
- Uses a custom directive where an argument would do