GraphQL Federation skill for Codex
One owner per field across every subgraph. Codex maps who owns each field in your supergraph, then adds entities and subgraphs with the right Federation 2 directives, batched __resolveReference lookups and nullable cross-subgraph fields. It composes locally with Rover, reads the query plan for extra hops, runs subgraph checks before publishing, and moves field ownership with @override instead of risky big-bang cutovers.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- graphql-federation Installs to ~/.agents/skills/graphql-federation/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install graphql-federation The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project graphql-federation Installs to .agents/skills/graphql-federation/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/graphql-federation.zipunzip -o graphql-federation.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) graphql-federation PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $graphql-federation, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds and evolves federated GraphQL supergraphs with Apollo Federation 2 - subgraph boundaries, entities with @key and reference resolvers, @shareable/@requires/@override, composition with Rover, schema checks in CI and router configuration. Use when splitting a monolithic GraphQL API across teams, adding a subgraph or entity, moving field ownership between services, debugging composition errors or slow query plans, or wiring rover subgraph check into a pipeline.
Use it when
- Adding a subgraph or entity to a federated GraphQL API
- Composition or rover subgraph check is failing
- Moving field ownership between teams or services
- Query plans through the router are slow or fan out too much
Use something else when
- A single GraphQL service with no federation: use graphql-schema-design
- Per-resolver batching and N+1 inside one service: use graphql-resolvers
How to check the result
rover supergraph compose and rover subgraph check pass, and a router query touching each changed subgraph returns data with the expected fetch count in its query plan.
The mistake it prevents
Making fields contributed by another subgraph non-null, so one subgraph outage fails every query that touches the entity.
A healthy supergraph has one owner per field, entities keyed by stable IDs, subgraphs that never call each other to answer a query, and composition plus breaking-change checks that run before any subgraph deploys.
Workflow
- [ ] 1. Map the current supergraph and ownership
- [ ] 2. Decide boundaries and entities
- [ ] 3. Write subgraph SDL with federation directives
- [ ] 4. Implement reference resolvers with batching
- [ ] 5. Compose locally and inspect query plans
- [ ] 6. Check, publish, then deploy
1. Map what exists
- Read
AGENTS.md, then locate each subgraph’s schema and the composition config (supergraph.yaml, GraphOS graph ref, Cosmo/Hive config). - Note the federation version in each subgraph’s
@linkand the router version. All directives you use must exist in the version the router and composition support. - List entities (
@keytypes) and which subgraph contributes which fields. A table of type, field, owner is the fastest way to spot accidental shared ownership.
2. Boundaries and entities
| Signal | Decision |
|---|---|
| A team owns the data and its lifecycle | That team’s subgraph owns the type’s core fields |
| Another domain needs to attach fields to the type | It contributes fields to the same entity via @key |
| A value object (Money, Address) appears in many subgraphs | Define it identically and mark @shareable |
| A subgraph only returns references to a type it cannot load | @key(fields: "id", resolvable: false) stub |
| Field needs data owned elsewhere to compute | @requires with @external fields |
| Two subgraphs would need a synchronous call to each other | Boundary is wrong; move the field or the data |
Keys: use the narrowest stable identifier (id), not mutable fields like email.
Compound keys (@key(fields: "sku region")) are fine when the identity really is compound.
3. Subgraph SDL
# products subgraph
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.7",
import: ["@key", "@shareable", "@inaccessible"])
type Product @key(fields: "id") {
id: ID!
name: String!
price: Money!
weightGrams: Int
}
type Money @shareable {
amount: Int!
currency: String!
}
type Query {
product(id: ID!): Product
}
# shipping subgraph
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.7",
import: ["@key", "@external", "@requires"])
type Product @key(fields: "id") {
id: ID!
weightGrams: Int @external
shippingEstimate: ShippingEstimate @requires(fields: "weightGrams")
}
- In Federation 2, contributing fields to an entity needs no
extend type. - Fields contributed by another subgraph should usually be nullable: if that subgraph is down, the router can null just that field instead of failing the parent.
- Hide internal fields from the public API with
@inaccessiblerather than removing them.
4. Reference resolvers
Every resolvable entity needs a reference resolver. The router sends batches of
representations to _entities, so batch the lookup.
import { buildSubgraphSchema } from '@apollo/subgraph';
import DataLoader from 'dataloader';
const resolvers = {
Product: {
__resolveReference(ref: { id: string }, ctx: Ctx) {
return ctx.loaders.productById.load(ref.id);
},
},
};
export const schema = buildSubgraphSchema({ typeDefs, resolvers });
// per request:
const loaders = {
productById: new DataLoader(async (ids: readonly string[]) => {
const rows = await db.product.findMany({ where: { id: { in: [...ids] } } });
const byId = new Map(rows.map((r) => [r.id, r]));
return ids.map((id) => byId.get(id) ?? null);
}),
};
Other stacks follow the same pattern: gqlgen generates Find<Entity>By<Key> resolvers,
graphql-ruby uses resolve_reference, Strawberry uses resolve_reference classmethods,
Hot Chocolate uses [ReferenceResolver]. Always return null (not throw) for missing IDs.
5. Compose locally
# supergraph.yaml
federation_version: =2.7.1
subgraphs:
products:
routing_url: http://localhost:4001/graphql
schema:
file: ./products/schema.graphql
shipping:
routing_url: http://localhost:4002/graphql
schema:
file: ./shipping/schema.graphql
rover supergraph compose --config ./supergraph.yaml > supergraph.graphql
Rover downloads the composition plugin on first run, which needs network; in a sandbox
without network, ask before running or use an already-installed plugin. rover dev
runs a local router against running subgraphs for end-to-end testing.
Inspect the query plan for every new cross-subgraph query (Apollo Sandbox or the router’s
query-plan exposure in dev). Each extra fetch node is a network hop; sequential fetches
chained through @requires add latency.
Common composition errors:
| Error theme | Fix |
|---|---|
| Field defined in multiple subgraphs but not shareable | Mark both definitions @shareable, or move the field to one owner |
| Type definitions differ between subgraphs | Make shared value types identical (fields, nullability) |
| Field cannot be satisfied from this entry point | Add a @key in the subgraph that needs to jump, or move the field |
| Directive not imported | Add it to the @link import list |
6. Check, publish, deploy
rover subgraph check my-graph@prod --name products --schema ./products/schema.graphql
rover subgraph publish my-graph@prod --name products \
--schema ./products/schema.graphql --routing-url https://products.internal/graphql
- Run
checkon every pull request; it validates composition and compares against real operation usage. Treat failures as blocking. - Order of operations for additive changes: deploy the subgraph code, then publish the schema. For removals: publish the schema without the field (after usage drops to zero), then remove the code.
- Publishing and production router changes affect live traffic; confirm with the user
before running
publishagainst a production graph ref.
Moving a field between subgraphs
- Implement the field in the new subgraph with
@override(from: "old-subgraph"). - Deploy the new subgraph code, then publish; the router now routes the field to it.
- Watch error rates and latency; roll back by removing
@overrideand republishing. - Remove the field from the old subgraph, then remove
@override.
Progressive override (@override(from: "products", label: "percent(10)"), Federation 2.7+)
shifts traffic gradually; confirm your router and plan support it before relying on it.
Router essentials
# router.yaml
headers:
all:
request:
- propagate:
named: authorization
include_subgraph_errors:
all: false
- Subgraphs must not be reachable from the internet; only the router should call them.
- Propagate only the headers subgraphs need; never forward cookies wholesale.
- Set per-subgraph timeouts and enable query depth/cost limits at the router.
Gotchas
- A subgraph calling another subgraph’s HTTP API to resolve a field reintroduces the distributed monolith federation was meant to remove.
- Non-null contributed fields turn one subgraph’s outage into errors on every query that touches the entity.
- Forgetting batching in
__resolveReferenceproduces one database query per entity in a list: classic N+1 across the network. @shareableon fields with different resolver logic in two subgraphs gives clients inconsistent answers depending on the query plan.- Federation 1 subgraphs (no
@link) compose under different rules; migrate them to Federation 2 syntax before adding new directives.
Verification
rover supergraph composesucceeds locally with no hints you have not reviewed.rover subgraph checkpasses against the target variant.- An end-to-end query through the router returns data from each touched subgraph, and the query plan shows the expected number of fetches.
- Kill one contributing subgraph locally and confirm the parent query still returns partial data with an error, not a total failure.
Output template
## Federation change
- Subgraphs touched: <names>
- Entities and keys: <Type @key(fields)>
- Field ownership changes: <field: old -> new, mechanism>
- Composition: <pass, hints reviewed>
- Check result: <link or summary>
- Deploy order: <steps> What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "GraphQL Federation"
short_description: "One owner per field across every subgraph"
default_prompt: "$graphql-federation Split the reviews part of our GraphQL API into its own subgraph" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.