Contextual Arguments
Contextual arguments let a field receive data from one of its ancestors in the response, without
the client ever sending it. A type declares a named context with @context, and a field argument
further down the graph declares where its value comes from with @fromContext. Hive Router fills in
the argument value from the nearest ancestor that sets the matching context.
This solves a common problem in federated schemas: a deeply nested field sometimes depends on data
that lives higher up in the response. Without contexts, you’d have to either expose that data as a
public argument the client must repeat, or push it into entity @keys so that every type along the
way carries data it doesn’t otherwise need.
Contextual arguments are part of Federation v2.8 and are supported by both Hive Router and Hive’s native composition.
Defining a context
Import @context and @fromContext in your subgraph schema:
@context(name: "userContext")onUsermakes everyUserobject in a response a provider of theuserContextcontext.@fromContext(field: "$userContext { currency }")on thecodeargument says: “take thecurrencyfield of the nearestuserContextprovider above me and use it ascode”.
The code argument is removed from the public API schema, together with the context directives.
Clients see only this:
Rules composition enforces
Composition checks contextual arguments before the router ever sees them. Each rule below is enforced by Hive’s composition, with the error it reports:
| Rule | Error when broken |
|---|---|
| The argument is nullable, even when the selected field is non-null. | CONTEXT_INVALID_SELECTION |
| The argument has no default value. | CONTEXT_NOT_SET |
| Every path to the field passes through a type that sets the context. | SATISFIABILITY_ERROR, “could not find a match for required context” |
@context and @fromContext are in the same subgraph. | CONTEXT_NOT_SET, “is never set” |
The type that declares the contextual field has a resolvable @key in that subgraph. | CONTEXT_NO_RESOLVABLE_KEY |
| A selection on a union context uses type conditions. | CONTEXT_INVALID_SELECTION |
| The field doesn’t implement an interface field. | CONTEXT_NOT_SET |
The first rule surprises most people. Composition types every context selection as nullable,
because the value can still be null at run time: a null anywhere along the selection, or an
ancestor whose fetch failed. So code: String! is refused even when currency is String!.
Handle null in the resolver.
The fields a selection reads don’t have to belong to the subgraph that sets the context.
Declare them @external there, and the router fetches them first. See
contexts across subgraphs.
Querying contextual fields
Clients query contextual fields like any other field:
Given two users, Alice with currency: "USD" and Bob with currency: "EUR", the subgraph resolves
Alice’s transactions with code: "USD" and Bob’s transactions with code: "EUR". Every entity
gets the value of its own ancestor.
How the router resolves contextual arguments
The router plans a contextual argument similar to a @requires, except that the required data
lives on an ancestor instead of the parent:
- Fetch the context value (
currency) together with the ancestor (User). - Fetch the contextual field (
amount) in a separate entity request after that. - For each entity, read the context value from the response and send it as a variable.
This is the query plan for the operation above:
Even though both fields live in the same subgraph, amount is fetched through a second _entities
request. The context value must exist in the response before the router can send it.
To see the plan for your own operation, set query_planner.allow_expose: true in the router
configuration and send the request with a hive-expose-query-plan: true header. The plan is
returned under extensions.queryPlan. Use it in development only.
Context rewrites in the query plan
The second fetch tells the executor where to find the context value relative to each entity. In the
JSON representation of the query plan, the fetch has contextVariables and contextRewrites:
Read the rewrite as: go up one object (Parent), check that it’s a User, read its currency, and
send the value as $__context_0.
The nearest ancestor wins
When the same context is set several times along a path, the value always comes from the closest ancestor. Consider users that have friends, who are users too:
Each transaction gets the currency of the friend it belongs to, not of the outer user.
If the nearest ancestor can’t provide the value, the field fails to plan. The router doesn’t fall back to an ancestor further up. This follows the Federation specification and matches Apollo Router’s behavior.
Contexts on interfaces and unions
A context can also be set on an interface or a union. Use type conditions in the selection to read fields from the concrete type the ancestor turns out to be:
Only the branch matching the actual type of the ancestor is used. Transactions of a User get the
user’s currency, and transactions of an Organization get the organization’s currency.
Contexts across subgraphs
The ancestor providing the context and the contextual field can live in different subgraphs. The
subgraph defining the contextual field declares the context fields it reads as @external:
For this query:
The router fetches the currency from accounts, then resolves lastTransaction and the contextual
field in a single request to transactions:
The query planner merges contextual fetches into other fetches whenever it’s safe to do so. Here,
the contextual fetch for Transaction merges into the entity fetch for its ancestor User, which
saves a round trip. A contextual fetch is never merged with the fetch producing its context value,
since the value has to exist before the request is sent.
One request for many context values
Contextual arguments make entity fetching harder. Entities in a list usually share one _entities
field, but entities whose ancestors hold different context values can’t, because each needs a
different argument value. The straightforward approach is to send one subgraph request per distinct
value. A list of users in 20 currencies then turns one entity fetch into 20 HTTP requests to the
same subgraph, and the number grows with your data.
Hive Router avoids this. It groups the entities by their context values and sends all groups in a
single subgraph request, each group as its own aliased _entities field:
| Distinct context values | One request per value | Hive Router |
|---|---|---|
| 1 | 1 request | 1 request |
| 2 | 2 requests | 1 request |
| 20 | 20 requests | 1 request |
Fewer requests mean fewer HTTP round trips, less connection pool pressure, and less per-request overhead on both the router and the subgraph, which can resolve all groups in one execution.
For the users example above, with Alice in USD and Bob in EUR, the subgraph receives one
request:
The first alias receives only the transactions of USD users, the second only the transactions of
EUR users. The router then maps the responses and errors back to the original positions in the
client response, so aliasing is completely invisible to clients. When all entities share the same
context values, the router sends the original, non-aliased operation.
On top of that:
- It works together with entity batching. Contextual fetches to the same subgraph from different parts of a query are combined into one batched request, and every context group repeats each batched fetch inside that same request. Instead of multiplying requests, contexts and batching share a single round trip.
- Entities are deduplicated per group. A transaction that appears several times under users
with the same currency is sent once. The same transaction under a
USDuser and anEURuser is sent once in each group, because the results can differ. - Building the aliased query is cheap. The query planner compiles each contextual fetch into a template once. At execution time, the router writes the query for any number of groups by copying strings, without parsing, re-planning, or caching a separate query per group count.
- Grouping is fast. Entities under the same ancestor are usually next to each other in the response, so the router reuses the previous entity’s group without reading and hashing the context values again. Context values are borrowed from the response instead of copied.
Limiting aliases per request
The number of groups depends on the response data, not on the query. A list with 50 users in 50
different currencies produces 50 aliased _entities fields. Subgraphs that limit aliases in an
operation (for example, with graphql-armor’s max-aliases rule) can start rejecting these requests
when the data changes.
Use traffic_shaping.max_aliases_per_request
to cap how many aliased _entities fields the router puts in a single subgraph request:
With a cap of 5, a fetch with 12 distinct context value groups is sent as three requests with 5,
5 and 2 groups. These requests run in parallel, so splitting doesn’t add round trips one after
another. The option is not set by default, so all groups of a fetch go in one request.
Behavior details
Missing values are sent as null
If the ancestor’s context field is missing or null, or a null sits anywhere along the selection
path, the router sends an explicit null for the argument. The variable is always present in the
subgraph request, so the resolver sees null rather than an absent argument. Composition only
accepts nullable contextual arguments, so this is always a valid value.
Non-null contextual arguments are not combined
Composition refuses a non-null contextual argument, so a supergraph composed by Hive or Apollo
never contains one. The router still handles a supergraph that does, for example one written by
hand: fetches with a non-null contextual argument (code: String!) are never combined into one
request with other groups or fetches, and each context value group is sent as its own request, in
parallel.
Combining requests sometimes requires the router to send null for a context variable that a
particular part of the request doesn’t use. A non-null variable would make the whole request fail
variable coercion. Keeping these fetches separate also prevents one group with a missing value from
failing unrelated groups.
Mutations
A mutation root field whose result contains contextual fields finishes resolving them before the next mutation root field runs, preserving the sequential execution order of mutations.
Contextual fields inside @requires
A @requires selection can include a contextual field, as long as the context is set within the
required selection itself. For example, if Plan.discount requires offer { value }, then
Offer.value can read a context set by Plan. The router fetches the context, then value, then
discount.
If a contextual field selected by @requires (or by another @fromContext selection) needs a
context set above the starting point of that selection, planning fails with:
Performance considerations
- A contextual field costs a second entity fetch. The context value has to be in the response before the request that uses it is sent, even when both fields live in the same subgraph. Across subgraphs, the planner merges the contextual fetch into the entity fetch for its ancestor where it’s safe.
- The number of aliases grows with your data, not your query. Each distinct context value
is one aliased
_entitiesfield per fetch. A list of 200 users in 200 currencies is one request with 200 aliases. If entity batching combines several fetches into that request, every group repeats each fetch’s alias, so three batched fetches make 600. Setmax_aliases_per_requestfor every subgraph that limits aliases. - An entity is resolved once per group. The same entity under two ancestors with different values is resolved twice, once in each group, because the results can differ.
- The request count only grows past the cap. With no cap, every group of a fetch that can be
combined goes in one request. With a cap, groups that don’t fit go in additional requests that
run in parallel. A cap of
1sends one request per distinct value. Two cases always send one request per group: non-null contextual arguments, and the variable-name collision below. - Client variables can prevent aliasing. If a client variable that reaches the contextual
fetch is named like a generated one, such as
representations_1or__context_0_1, the router falls back to one request per group. The answer is unchanged.
Checklist
Use this list when you write or review a schema with contextual arguments, by hand or with an AI agent:
- Declare the contextual argument nullable, with no default value.
- Make sure every path to the field passes through a type that sets the context.
- Give the type that declares the contextual field a resolvable
@keyin the same subgraph. - Put
@contextand@fromContextin the same subgraph. Read fields that another subgraph owns by declaring them@externalon the context type. - Use type conditions when the context is set on a union.
- Expect the nearest ancestor’s value. The router never falls back to an ancestor further up.
- Handle
nullin the resolver. - Serve the supergraph with Hive Router 0.3.6 or later.
- Set
max_aliases_per_requestfor subgraphs that limit aliases. - Test a list whose entities have ancestors with different context values, not only a single
entity. That’s the case that needs more than one
_entitiesfield.