Skip to content

GraphQL directives

GraphQL only A plain HTTP API has no schema; it uses Cache-Control and the Surrogate-Key header instead.

GraphPilot reads these directives from the schema you deploy, when it builds the deployment. None of them is forwarded to or needed by your origin. What each one is for is on the pages linked from each section; this page is the exact surface.

DirectiveDecidesConcept
@cacheControlHow long a response lives and who shares itLifetime from the schema, scopes
@varyWhat a named scope partitions onScopes in the schema
@surrogateKeyWhich surrogate keys a response carriesKeys from the schema
@auth, @graphpilotPolicyNothing: parsed and reported, not enforced

No finding on this page fails a deployment. Each is recorded on the deployment as a diagnostic, shown in the portal under Schema → Findings, printed by gpilot deploy, and returned by the GraphPilot API.

schema.graphql
directive @cacheControl(
maxAge: Int
scope: CacheControlScope
inheritMaxAge: Boolean
swr: Int
) on OBJECT | INTERFACE | UNION | FIELD_DEFINITION
enum CacheControlScope {
PUBLIC
PRIVATE
}
ArgumentTypeDefaultMeaning
maxAgeIntThe default of the maxAge argument in your definition, else 0The freshness lifetime in seconds. An explicit maxAge always wins over inheritance, on the type or the field.
scopeCacheControlScopePUBLICWho may be served the entry: PUBLIC, PRIVATE, or a named scope you declare on the enum. Omitted means PUBLIC, as in Apollo Server.
inheritMaxAgeBooleanfalseOn a non-root field with no maxAge, take the parent field’s effective maxAge instead of the default. No effect on a root field.
swrIntnoneSeconds past maxAge an entry may still be served as STALE while a fresh copy is fetched. A field that states no swr does not switch the window off for the query. See stale windows.
LocationEffect
OBJECT, INTERFACE, UNIONApplies to every field that returns this type, unless that field has its own hint. On a member of a union or an implementor of an interface, the scope also joins every field returning that union or interface; its maxAge does not. See a field returning a union or interface.
FIELD_DEFINITIONApplies to that field only. Each argument it sets overrides the return type’s hint; each one it leaves out, scope included, comes from the type. See a field hint overrides only the arguments it sets.

The fallback for an unannotated root or composite field (Apollo’s defaultMaxAge) is the default value you declare for the maxAge argument on the directive itself:

schema.graphql
directive @cacheControl(
maxAge: Int = 30 # unannotated composite fields fall back to 30, not 0
scope: CacheControlScope
inheritMaxAge: Boolean
swr: Int
) on OBJECT | INTERFACE | UNION | FIELD_DEFINITION

maxAge: Int with no default leaves the fallback at 0, which is uncacheable, as in Apollo Server. A non-integer default is ignored and also leaves it at 0.

WrittenReported asEffect
@cacheControl used without a matching directive definitionErrorThe hints are still resolved.
On any other location (a scalar, an enum, an input type or field)ErrorThe hint is dropped.
On a mutation fieldWarningThe hint is dropped; a mutation is never cached.
An argument name other than maxAge, scope, inheritMaxAge and swr (or your configured names)ErrorThe whole hint is dropped, and the build names the argument, so a typo such as maxage is reported rather than passing in silence.
An argument of the wrong type: a maxAge or swr that is not a whole number from 0 to 4294967295, an inheritMaxAge that is not a Boolean, a scope that is not an enum valueErrorThe whole hint is dropped.
A scope value other than PUBLIC and PRIVATE that carries no @vary, or whose @vary is malformedErrorThe hint is kept with the scope read as PRIVATE, the narrowest audience it could have meant.
A field returning a union or an interface whose members state a lower maxAge than the field resolves toWarningNothing changes: the response is cached for the field’s maxAge. The warning names the field and each member below it. A field that inherits its parent’s maxAge is not compared.

Declares a named scope: a value of your own CacheControlScope enum that partitions on the vary rules it lists.

schema.graphql
directive @vary(names: [String!]!) on ENUM_VALUE
enum CacheControlScope {
PUBLIC
PRIVATE
ORGANIZATION @vary(names: ["x-org"])
ORGANIZATION_ROLE @vary(names: ["x-org", "x-role"])
}
ArgumentTypeMeaning
names[String!]!Header names, matched case-insensitively against the name of your [[cache.policy.vary]] entries.
RuleDetail
LocationENUM_VALUE, on the enum your scope argument declares. PUBLIC and PRIVATE take no @vary: their meaning is built in, and one written on either is ignored.
A malformed namesMissing, not a list of strings, or empty: an error, and every scope naming that value is read as PRIVATE.
A name with no [[cache.policy.vary]] entryNot a deployment failure. Every response in that scope is refused (CACHE_SKIPPED_PRIVATE_WITHOUT_DISCRIMINATOR), and the build reports it at the annotation. See what the deployment build reports.
A name whose [[cache.policy.vary]] entry is not a legal header nameThe same: the entry is skipped, every response in that scope is refused, and the build reports it at the annotation.
A name in the gp- namespaceThe build reports it at the annotation, and the deployment does not fail. With a [[cache.policy.vary]] entry of that name, the scope still partitions on it: GraphPilot strips the namespace from what the client sends, then derives the entry’s value. Without one, it is refused like any name with no entry. Rename it outside the namespace anyway: the namespace is also stripped before your origin, so the entry’s pass_to_origin has no effect.
CompositionThe names of every scope a query selects are unioned. See how scopes compose.
schema.graphql
directive @surrogateKey(
static: String
of: String
ignore: Boolean
) repeatable on SCALAR | OBJECT | FIELD_DEFINITION | INTERFACE | UNION | ENUM

The definition may be made more restrictive (String to String!, for example), not extended to new types or locations. A bare @surrogateKey, with no argument, marks a field, scalar or enum as a key field. Examples for every location are on keys from the schema.

ArgumentTypeMeaning
staticStringOne or more fixed keys, separated by whitespace. See a group you purge by hand.
ofStringThe fields that form the key, as a GraphQL selection set ("user { id }") or in path syntax ("user.id"). Several fields form a composite key; the two notations can be mixed.
ignoreBooleantrue switches the automatic key rules off where the directive sits. See below.
RuleDetail
Leaf selections onlyEach path ends on a scalar, an enum, or a list of these ([String!], [Role!]), never on an object, interface or union.
Fields without required argumentsA field whose arguments all have defaults, or that has none.
No globs or recursion*, ** and recursive selections are not supported; each path names a field of the enclosing type.
Only where a traversal is possibleOn an object, interface or union. Elsewhere the argument is ignored.
A nullable pathAllowed, and reported as a warning: an object whose value on that path is null is still tagged, with an empty value (Book:isbn: for @surrogateKey(of: "isbn") on Book), so it cannot be purged by identity. A path is nullable when any step of it is: author.id is null whenever author is.
Written onEffect
An OBJECT or INTERFACENo field of that type is keyed by the automatic rules.
A field of an object type or interfaceThat one field is not keyed by the automatic rules.
A SCALAR, ENUM or UNIONNothing. The build reports a warning.
RuleDetail
Only the automatic rulesAn exclusion never removes a key you wrote. @surrogateKey(ignore: true) alone is the opt-out and nothing else; it is not read as a bare @surrogateKey.
Beside an explicit key@surrogateKey(ignore: true, of: "slug") swaps the automatic keys for the one written, with no warning.
InterfacesAn interface’s exclusion does not reach the types implementing it.
ValuesOnly true switches anything off. false is the same as leaving the argument out; any other value is a schema error and the automatic keys stay.
Against the configurationWhen surrogate_key.ignored_types or ignored_fields excludes something a directive keys explicitly, the directive wins and the build reports a warning.

The build refuses the directive, with the type, the directive and its line and column, and the deployment still succeeds:

  • an of naming a field that does not exist, or a path that does not end in a scalar, enum or list thereof;
  • an argument of the wrong type;
  • a bare @surrogateKey on a type that needs a selection;
  • a directive written where no key can come from, such as inside extend type or on a field argument.

Each directive can be read under a name of your own, configured in your service configuration. The keys and their rules are on [surrogate_key] and [cache_control].

Directive and argumentRenamed by
@cacheControl[[cache_control.directives]] directive_name
maxAge, scope, inheritMaxAge, swrmax_age_arg, scope_arg, inherit_max_age_arg, swr_arg
@vary and its namesvary_directive_name, vary_names_arg
@surrogateKey[[surrogate_key.directives]] directive_name
static, of, ignorestatic_arg, of_arg, ignore_arg

To read Apollo Federation’s @key(fields:) as a surrogate key directive, for example:

gpilot.toml
[[surrogate_key.directives]]
directive_name = "key"
of_arg = "fields"
schema.graphql
directive @graphpilotPolicy(dsl: String) on ARGUMENT_DEFINITION | ENUM_VALUE

@graphpilotPolicy(dsl:) attaches a policy expression to a directive argument or to an enum value, so that a directive such as @auth(requires: [Role!]) can state access rules in the schema.