Class 1: The Schema-as-Tool-Spec Mental Model
Duration: ~50 minutes | Difficulty: Advanced | Prerequisites: Working knowledge of GraphQL and Spring Boot, plus familiarity with at least one agent framework (Spring AI, LangChain4j, or an equivalent)
This lesson is reading only, and no part of the course needs a GraphQL backend of your own. The running example is the Movie Database service. From Class 2 onward the hands-on work happens in our project, graphQLBeyondMCP, starting from its main branch. If GraphQL is the side you are least sure of, our Introduction to GraphQL course covers the type system, schema descriptions and introspection that the lessons ahead assume you can read. If you have not built a GraphQL service on Spring before, our Spring GraphQL tutorial is where the Movie Database comes from, so it also shows the backend that these lessons call.
If you have built an LLM agent before, with Anthropic's tool use API, OpenAI's function calling, Spring AI, LangChain4j or a custom orchestrator, you have written the same thing by hand many times. You wrote a JSON Schema for a tool's input. You wrote a natural-language description of what the tool does. You wrote the shape of what comes back. Then you tested whether the model picked the right tool and passed the right arguments, and rewrote the description until it did. At ten tools, that is a small documentation project. At a hundred tools, it takes time away from the rest of your engineering work.
GraphQL already does almost all of that work, by design. It was built to be a typed, introspectable, self-describing graph of operations, with human-readable documentation on every type, field and argument. This course starts from one idea: a well-designed GraphQL schema is already an agent tool catalog, apart from a thin adapter layer. The descriptions you wrote for your frontend developers are prompt material. The argument types you wrote for your codegen are input schemas, and the return types are output schemas. The deprecation markers are stale-tool flags. Authorization metadata exposed through directives is access-control material the agent loop can reason about.
This course is about turning that latent agent tool catalog into an actual one, and doing it safely and economically. Tool catalog is this course's own word, and it means one thing throughout: the set of tool definitions an application advertises to a model, each with a name, a description, and a JSON Schema for its arguments. Other vocabularies name the same thing differently. MCP servers answer a tools/list request with it, and Spring AI calls the individual entries tool callbacks. This lesson explains the mental model. The ten lessons after it turn the model into infrastructure: generators, allow-lists, framework integrations, cost accounting and tests. The final lesson is an epilogue on the code-execution alternative to tool calling. By the end of this lesson you should be able to look at any GraphQL schema and find the tool definitions in it.
What Every Agent Tool Spec Needs
The Anthropic tool use documentation describes a tool definition with three parts. Two are required: a name, and an input_schema (a JSON Schema describing the arguments). The third, the description, is optional in the API, but it matters most in practice. The docs call it "by far the most important factor in tool performance", because the model reads it to decide when to use the tool.
OpenAI's function calling API uses almost the same shape: name, description, and a JSON Schema for parameters. The Vercel AI SDK wraps both in a unified tool() helper, and LangChain's Tool and StructuredTool classes follow the same pattern. The formats differ, and the contents are the same. Every agent framework needs four kinds of information about a tool:
- A name the model can refer to when it decides to call the tool.
- A natural-language description the model uses to decide whether to call the tool.
- A typed input specification the framework uses to validate the model's arguments before dispatching.
- A predictable output shape (sometimes typed, sometimes not) so the model can reason about what came back.
A typical GraphQL schema already has all four:
| What a tool spec needs | What the schema already publishes |
|---|---|
| A name | The operation's name: movie, search, createReview. |
| A description | The description: String the specification attaches to every type, field and argument, if your team has been disciplined about writing them. Where they have not, the names alone are often enough to start from. |
| A typed input | The argument list, with each argument's GraphQL type. An "untyped argument" does not exist in GraphQL. The types compose (lists, non-null wrappers, input objects, enums, scalars) in the same way JSON Schema composes, so the translation is a small one. |
| A predictable output | The operation's return type, itself a type in the schema, with all of its fields described, often in more detail than an agent framework uses. |
Every agent framework asks you to write these four things by hand, and a GraphQL schema already publishes them through introspection. What remains is a thin adapter that makes the model's job easier, and the rest of this course builds it.
Descriptions as Prompt Material
For agents, the description field is the most valuable metadata in a GraphQL schema. Every type, field, argument and enum value can carry a description, and that description ends up in two places. It appears in the introspection result, where your adapter reads it, and in the prompt the model sees, where it drives tool selection.
Most production GraphQL schemas have descriptions written for human developers. They are terse, sometimes idiomatic, sometimes phrased as if everyone reading them already understands the domain. "Returns user." "Get movies." "Cancels the order." These descriptions are fine for someone with the schema open in a code editor and an internal wiki in another tab. They work poorly for a language model that has to choose between forty operations with similar names, with limited time and tokens.
Consider the difference between two descriptions for the same field:
type Query {
"Get movies."
movies(filter: MovieFilter, page: Int = 0, size: Int = 10, sort: MovieSort): MoviePage!
}
versus:
type Query {
"""
Returns a page of movies. Pass a `MovieFilter` input to constrain by release
year window, genre, or minimum rating. Use `page` and `size` for pagination;
page numbering starts at 0. If `filter` is omitted, the whole catalog is
paged in the default sort order; pass `sort` to control ordering. Use
`search` for free-text search instead.
"""
movies(filter: MovieFilter, page: Int = 0, size: Int = 10, sort: MovieSort): MoviePage!
}
The first description is enough for a developer. The second is enough for an agent, because it does three things:
- It says what the field is for and what the inputs mean.
- It says what happens when arguments are left out.
- It names the field to use for a different need.
Those three properties turn documentation into prompt material.
Most GraphQL schemas in production have descriptions that were written for human developers, and those descriptions work poorly as agent prompts. Rewriting them for agents is the cheapest large improvement available once you have committed to the schema-as-tool-spec model. Make them clearer, more directive, less idiomatic, and explicit about defaults and side effects. Description quality decides whether the model picks the right tool.
The GraphQL AI Working Group's benchmark work, covered below, gives teams a shared way to measure this. Many teams find the problem after they deploy, when their agent calls getOrder where getOrderStatus was the right choice. Both descriptions sound plausible for a status request, and neither says which one to use. Class 2 makes this concrete. For now, notice that your existing descriptions are an asset, and that improving them helps quickly.
The Typed-Arguments Property
The second part of a tool spec is the input schema. With OpenAI's function calling, you author this as a JSON Schema object embedded in the function definition. With Anthropic, it is the input_schema field. With the AI SDK, you usually write a Zod schema and let the SDK convert it. In every case you are hand-writing a type system for the inputs of one operation, then writing it again for the next, and again for the one after that.
GraphQL schemas already have this type system. Every argument has a type. Scalar types (Int, Float, String, Boolean, ID) map directly to JSON Schema primitives. Enum types map to JSON Schema enum constraints. Input object types map to JSON Schema objects with properties and required. List types map to JSON Schema arrays with items. The non-null wrapper maps to the required array. The translation is mechanical, so a generator can do it. We build that generator in Class 2.
A typed argument list also lets you validate inputs before the model's call reaches your backend. Agent frameworks enforce input schemas with different strictness, but the pattern is the same. The framework parses the model's tool call and checks the arguments against the schema. On failure it rejects the call, corrects it, or asks the model to retry. A GraphQL-backed tool catalog adds a second check without extra work, because the GraphQL engine rejects malformed inputs with a validation error before they reach your resolvers. You end up with two layers: the framework validates at the agent loop boundary, and the GraphQL server validates again at the query boundary.
At best, a REST endpoint comes with an OpenAPI document, and many OpenAPI documents are not maintained, complete, or current. Without one, you write a JSON Schema for the endpoint's parameters yourself, and work out the response shape from documentation. A good OpenAPI document brings REST to the level GraphQL has had since 2015. In GraphQL, the types are part of the API itself, so the schema is the spec. They are the source of truth, and introspection gives them at runtime to any client that asks, agent clients included.
The Partial-Result Property
A third property of GraphQL schemas matters for agents more than for other clients: the ability to deliver partial results. GraphQL's @defer directive is still an open specification proposal, a Stage 2 draft of the incremental delivery RFC. It shipped as an experimental feature in graphql-js v17 in June 2026, and Apollo Router, Hot Chocolate and GraphQL Yoga support it. It allows a server to ship some fields of a response immediately and others later in the same operation, as a stream of incremental payloads. The client receives the initial part, can start processing it, and then receives the deferred parts as they become ready.
For a traditional UI, @defer is useful but optional: the page can show its top part while the slower fields stream in. For an agent, it can change how the loop runs.
The agent's loop fits the pattern @defer supports: pick an operation, execute it, receive results, reason, pick the next operation. If the first half of the result is enough for the next decision, the agent can start reasoning about it before the slow half arrives. It can even start the next tool call.
Add model-output streaming on the answer side, and three things happen at once. The server still delivers the slow fields, the model already reasons about the fast ones, and the user already watches the answer appear. Class 9 covers where each part of that pipeline stands on the JVM today. For now, remember that GraphQL is one of the few API layers where this is possible, and that the schema-as-tool-spec model extends to streaming tools.
What Makes a Good Agent Tool
Any GraphQL operation can become an agent tool, so which ones should? A good agent tool has the following properties, whatever the transport.
-
Clear naming. Query fields are nouns describing what they return (
movie,movies,search); mutation fields lead with a verb (createReview,createWatchList). Within that convention, prefer names that describe what the operation does over names that describe how:movieByIdis good;fetchMovieRecordis worse;dataLoaderMovieis confusing. This is the naming guidance from our schema design best practices, applied to a new audience: an LLM that has read thousands of schemas in its training data and forms expectations based on common patterns. -
Good descriptions. These are covered above. On most existing schemas, better descriptions are the change that helps fastest.
-
Sensible defaults for optional arguments. When the model omits an argument, your resolver should do something reasonable. If
movies(filter: MovieFilter)returns the entire catalog whenfilteris null, that may or may not be what the agent wanted; document it. If it returns an error, document that too. Hidden default behavior is the most common source of agent-loop confusion in our experience. -
Predictable error shapes. When something goes wrong, the agent needs to know whether to retry, give up, or try a different tool. GraphQL's standard
errorsarray is fine for transport errors. For business-logic failures, such as the movie was not found, the user is not authorized, or the inventory is empty, you want errors-as-data: typed error union types in the schema, the way our introduction's errors-as-data strategies cover in detail. The agent reads the union, decides which branch was returned, and acts accordingly. This is the same principle as result types in Rust or Swift: make the failure modes part of the type. -
No hidden side effects. A query that changes state breaks GraphQL's own contract. It also breaks the agent's safety model, because an agent that believes it is only reading cannot safely roll back what the query changed. Class 4 covers the broader pattern of restricting agents to queries by default and gating mutations behind explicit confirmation.
The Agent's Perspective
An LLM agent receives a prompt, a list of tool definitions, and, over several turns, the results of earlier tool calls. On each turn, it either writes a final response or calls a tool. When it calls a tool, it produces a structured object with the tool name and the arguments. Some framework or runtime then dispatches that call, returns the result, and the loop continues.
The diagram shows one turn of that loop, with the GraphQL side of each step named. Every lesson ahead either adds a control to one of these arrows or changes which component runs the loop, so study it before you go on:
Two details in the diagram matter later:
- The whole conversation, including every earlier tool result, is sent again on each pass. Class 10's cost arithmetic starts from this.
- The model never calls the GraphQL server directly. Your runtime sits between them, and every safety control in Class 4 goes there.
From the agent's perspective, a tool call is a typed function call. It picks a function by name from a catalog and supplies arguments matching that catalog's type spec. It receives a result with a known shape, and reasons about that result to decide what to do next. That also describes a GraphQL operation, and the match holds term by term:
| What tool calling needs | What GraphQL already has |
|---|---|
| a catalog of callable things | the schema |
| the functions themselves | the operations |
| input schemas | the argument types |
| output schemas | the return types |
| a way to discover the catalog | introspection |
| predictable failures | errors-as-data |
| partial results that stream | @defer |
The rest of this course builds what is still missing: mostly translation, curation, and the safety controls around both.
One more property of this client matters now, because Class 4 builds its whole safety design around it: an agent keeps calling and keeps exploring. A human integrator tries a handful of requests during development and then settles into habits. An agent eventually uses everything its tools allow, at machine speed, and it does not get tired. The controls around the agent decide whether that thoroughness helps or causes harm.
The Contrast with REST and the Composition with MCP
REST gives you endpoints, sometimes with an OpenAPI document. The endpoints are typed only if that document is maintained, complete, and current, and many documents miss at least one of those. Even with a good OpenAPI document, you still write a separate adapter for each agent framework, to translate OpenAPI components into the framework's tool definition format. GraphQL gives you the same information as part of the API. It is always current, because it is the schema itself, and you can query it at runtime.
The Model Context Protocol (MCP), covered in our MCP course, sits at a different layer. MCP defines a protocol for how an LLM client (Claude Desktop, Cursor, an in-house agent) discovers and invokes tools, resources, and prompts on a server. GraphQL defines a schema for how a backend exposes operations. The two are often discussed together, and they work as layers, one on top of the other:
An MCP server can be a thin wrapper around a GraphQL endpoint that translates MCP tool calls into GraphQL operations. The MCP course builds that pattern. This course covers the wider case: how to expose GraphQL to any agent framework, MCP-aware or not, with the same schema-as-tool-spec model. If you have done the MCP course, much of what follows will look familiar. The new parts are:
- the adapter work across frameworks
- the cost accounting
- the safety patterns for agents that do not run behind an MCP-aware client
The Standards Still in Motion
The schema-as-tool-spec mental model has moved beyond vendor blog posts and into standards-body work. The GraphQL Foundation announced an AI Working Group in October 2025. Like every GraphQL working group, it is open to anyone working on the problem, so its participants come from vendors, large GraphQL adopters and independent practitioners. The group's November 2025 recap states its primary goal: reducing incorrect field selection, invalid queries, and fabricated schema elements when LLMs generate GraphQL operations. Through 2026 the group has met monthly in public, and the ai-wg repository holds the agendas, notes, and RFCs. Two parts of its work matter for the rest of this course.
The first is schema navigation for agents. The flagship proposal is semantic introspection, a standardized search field in the introspection schema. It lets an agent find relevant types and fields by natural-language intent, rather than loading the whole schema into context. Hot Chocolate v16 shipped a product implementation of the idea in May 2026. The RFC describes MCP tools and GraphQL root fields as the same kind of thing.
Alongside it, an open fine-tuned embedding model for schema retrieval was announced on graphql.org in July 2026. It outperforms general-purpose embeddings at mapping questions to Type.field coordinates, and its benchmark comparison merged into the working group's repository in June 2026. A @require_graphql_subschema directive for declaring context-window-sized subschemas is still an exploratory idea in the group's issue tracker. All of this is at the proposal stage, so follow it and wait before you build against it. If semantic introspection is adopted, the role-based filtering work in Class 2 becomes closer to a schema annotation than a custom generator.
The second is benchmarking. A benchmark suite of 816 natural-language questions with ground-truth Type.field answers has been arriving in the group through pull requests since mid-2026. It covers five production-scale schemas: GitHub, GitLab, Linear, Shopify and Singapore's Open Data platform. The model comparisons have merged, and the evaluation suite that scores them is still open.
The benchmark is the measurement behind the embedding model's claim to beat general-purpose embeddings, and Class 3 reports the actual numbers, alongside its own measurements against the same data. Earlier, in its November 2025 meetings, the group shared preliminary comparisons of how well different LLMs write GraphQL operations. Those early results had Claude Sonnet 4.5 strongest on correctness and schema adherence at the time. They predate both the current model generation and any reliable way to score the answers automatically, so they are too old to tell you which model writes the best GraphQL today.
The patterns this course teaches build on standards that are starting to take shape. There is a public working group, a benchmark, and a new graphql.org/ai page collecting the material. The work is portable across frameworks today and will become more portable as the proposals mature.
What If GraphQL Is Not the Right Abstraction?
A course that argues "GraphQL is unusually good as an agent tool catalog" should also present the strongest opposing view. Hasura's move from GraphQL-as-product to PromptQL states it most clearly.
Hasura, historically the company most associated with instant GraphQL APIs, announced PromptQL in late 2024 and completed the repositioning with From GraphQL to PromptQL in mid-2025.
Hasura's PromptQL material, taken as a whole, makes this argument. Free-form GraphQL is too unconstrained for an LLM to execute reliably. A schema-aware LLM is still an LLM, and its generated queries fail in production in familiar ways:
- overfetching
- underfetching
- calling expensive operations by accident
- inventing fields that almost exist
Hasura's answer is a deterministic plan-execution layer between the LLM and the database. The LLM composes a plan in a DSL designed to be unambiguous, and infrastructure executes it, enforcing access control, redaction and cost limits where the LLM cannot see or influence them. Hasura argues that combination is more trustworthy than free-form GraphQL with best-effort guardrails.
The position is worth taking seriously for two reasons. First, Hasura is an insider making an insider's argument. They sold the strongest version of "instant GraphQL" for nearly a decade, and moved away from it deliberately, knowing GraphQL's strengths. Second, the failure modes they cite are real. The patterns this course teaches reduce those failures, and they do not remove them. Those patterns are allow-lists in Class 4, complexity caps and persisted queries. An agent can still call the wrong operation with arguments that pass every validation.
This course still recommends GraphQL, for a practical reason. Most teams reading this already have a GraphQL service. The marginal cost of adding agent integration is small if the schema is already good and the safety patterns are layered on top. Adopting a separate DSL stack is a much larger change, and nobody has yet shown that it solves problems your existing infrastructure cannot solve. Read Hasura's blog post and decide for yourself. If PromptQL solves your specific problems better, use it. If your existing GraphQL schema is the asset, the rest of this course shows how to use it safely as an agent tool catalog.
Security and Cost Implications
The schema is the agent's contract. If the agent can run introspection against your endpoint, it can discover every operation, every type, every field. For an internal agent that you trust to read everything you have ever exposed, that is fine, even desirable. For an agent driven by user input, where the user could in principle steer the agent toward operations you did not intend it to use, it is a problem. The standard safety pattern has three parts:
- turn off introspection in production, which Spring Boot does with one property, as Class 13 of our Spring GraphQL course shows
- give the agent a curated allow-list of persisted operations, which Class 2 builds
- refuse every operation that is missing from the list, which the tool layer in Class 4 does
The schema is still the source of truth for tool definitions, and the live endpoint exposes only the part you have explicitly approved.
There is a cost dimension here too. GraphQL's complexity model lets you attach a cost to each field and a budget to each operation, and with agents that matters in a way it does not for human-driven clients. A human who asks for 100 movies with all their reviews by accident waits through one slow page and learns to avoid it. An agent does not learn from the wait.
The same expensive call goes out on every iteration of the loop, at machine speed, and the bill grows before anyone checks it. Because the cost annotations are in the schema, an adapter can tell the model what an operation costs before it runs. For example: "this operation costs 200 complexity units, so prefer a cheaper alternative where one exists." Class 10 builds the per-run accounting for that.
This limit applies to every claim of that kind in this lesson, including the authorization example above: an adapter can read directives only if it reads the SDL. Introspection reports directive definitions in __schema.directives, but it does not report where a directive was applied. An agent that discovers your schema through introspection sees the field and its type, and it does not see the @auth or @cost you attached. When that metadata matters to the agent loop, feed the loop SDL, or move the fact into the description, which introspection does carry.
A Worked Example
Here is part of the Movie Database schema that this course uses throughout. The service comes from the Spring GraphQL tutorial, and Class 2 shows how to clone and start it. With agent-friendly descriptions, the three main read operations look like this:
type Query {
"""
Returns a movie by its database identifier. Use this when you have an exact
ID (typically from a prior `search` or `movies` call). Returns null
if no movie exists with that ID; check for null before assuming success.
"""
movie(id: ID!): Movie
"""
Returns a page of movies, optionally filtered. Pass a `MovieFilter` input
to constrain by release year window, genre, or minimum rating. Use `page`
and `size` for pagination; page numbering starts at 0. If `filter` is null,
the whole catalog is paged. For free-text search across titles and names,
use `search` instead.
"""
movies(filter: MovieFilter, page: Int = 0, size: Int = 10, sort: MovieSort): MoviePage!
"""
Case-insensitive free-text search across movies, TV shows and people. Use
this when the user names something (or part of a name) instead of describing
it with structured criteria. Returns a union, so check `__typename` on each
result before reading its fields. For structured filtering of movies alone,
use `movies`; for exact-ID lookups, use `movie`.
"""
search(query: String!): [SearchResult!]!
}
Each of the three descriptions tells the agent what the operation returns, when to use it, and what to use in the obvious alternative cases. It also says what happens with default arguments, and what the failure modes look like. All of this lives in the schema itself, and the tool-spec generator in Class 2 reads it directly. The agent-friendly style is almost always the human-friendly style too, so better descriptions help human developers as well.
Summary
Every agent framework needs the same four things about a tool: a name, a description, a typed input, and a predictable output shape. A GraphQL schema already publishes all four through introspection, so with a thin adapter layer it becomes an agent tool catalog.
The description field carries the most weight. Descriptions written for human developers usually need a rewrite before they work as prompt material: more directive, explicit about defaults, and cross-referential. The typed argument list gives you input validation at two layers without extra work: the framework and the GraphQL engine. The still-experimental @defer directive extends the model to streaming partial results. The idea now has standards-body attention, since the GraphQL AI Working Group is developing semantic introspection, schema-retrieval embeddings and a public benchmark. Hasura's PromptQL is the serious counter-position, arguing for a deterministic plan layer in place of free-form GraphQL. This course's answer is that a team with a GraphQL schema already gets further with curated exposure and layered safety controls.
Further Reading
- Announcing the GraphQL AI Working Group (graphql.org, October 2025). The founding announcement, with the group's goals and how to join; meetings are public and the ai-wg repository has the agendas and notes.
- Semantic introspection RFC. The working group's proposal for natural-language search over introspection; the clearest statement of how the standards layer sees agents navigating schemas.
- A GraphQL-Native Embedding Model for AI Agents (graphql.org, July 2026). The open embedding model and the 816-question benchmark it is evaluated against.
- Anthropic: tool use overview and OpenAI: function calling. The two provider-native tool definition formats; useful background for the JSON-Schema-shaped contract the Class 2 generator emits.
- Hasura: From GraphQL to PromptQL. The strongest published argument against free-form GraphQL as the agent interface; worth reading in full before you commit to either path.
What's Next
Class 2 turns this mental model into code. We will build a generator that reads a GraphQL schema, picks a subset of operations to expose, maps the GraphQL types to JSON Schema, and emits tool definitions through Spring AI's ToolCallback contract. That contract carries the same JSON-Schema-shaped information every major framework and provider consumes. The schema-as-tool-spec model is the foundation, and generating the tool definitions is what makes it work at the scale of a real production schema.