Custom routes
Sooner or later the built-in API is not quite enough. You want a health endpoint your load balancer understands, a webhook receiver for a payment provider, a small internal endpoint that returns something your front end needs and no agent should be computing. The platform’s answer is to let you mount your own routes on the same server.
The key, and the family it belongs to
The http key in langgraph.json configures the server’s HTTP layer, and that is where custom routes are wired in. Look back at the four families from the config lesson: http sits in what runs, alongside graphs, ui, auth and webhooks. That placement is the fact worth carrying, because it tells you two things at once. Custom routes are code you are shipping, and they are declared at build time.
The sub-keys
The one that actually mounts your routes is app, and it takes a module path in the same file:variable shape the graphs key uses:
{
"http": {
"app": "./src/agent/webapp.py:app",
"enable_custom_route_auth": true
}
}The rest of the object sorts into three jobs, and sorting them that way is more useful than memorising sixteen names:
| Job | Sub-keys |
|---|---|
| Mount your own app | app, mount_prefix |
| Turn built-in routes off | disable_assistants, disable_threads, disable_runs, disable_store, disable_ui, disable_mcp, disable_a2a, disable_meta, disable_webhooks |
| Shape the request path | cors, configurable_headers, logging_headers, middleware_order, enable_custom_route_auth |
The disable_* family is worth one look, because it is the clearest statement anywhere in the config that the built-in API surface is a set of route groups you can subtract from. It is also the same vocabulary as the API groups: assistants, threads, runs, store.
Two notes on durability of this list. The TypeScript runtime currently supports a subset of these sub-keys, so a key existing in the Python schema does not guarantee it is honoured in a JS deployment; that is the same TypeScript-shaped hole this course opened with. And the exact set grows over time, so if a question turns on an unusual sub-key, confirm it against the CLI reference rather than this table.
Why this beats a sidecar
The instinct of most engineers is to run a small separate service next to the deployment. It works, and it costs you four things.
Mounted on the server, a custom route gets one URL, one credential story, one deployment unit, and one auth layer available to it. It ships in the same revision as the agent code, so it cannot drift out of step with the agent it exists to support. It shares the deployment’s environment and its access to the store.
Read that fourth item precisely, because the next section is about exactly how precisely. Mounting puts the auth layer within reach of your route. It does not put your route behind it.
A sidecar gets you a second thing to deploy, a second thing to secure, a second thing to keep in sync, and a second place for a tenant-scoping bug to hide.
The part that is a security question
Here is the fact most likely to bite you in production, and it is the opposite of what the architecture suggests.
Sitting on the same server is not the same as sitting behind the same door. The instinct is to reason that co-location implies inheritance, and it does not: the auth layer here is scoped to a specific mount rather than wrapped around the whole application. So the health endpoint you added, the webhook receiver, the small internal endpoint that returns something your front end needs, are all open until you say otherwise.
That makes two distinct failure modes rather than one, and the exam-relevant one is the first:
- The route is unauthenticated. You never set the flag, so no identity is established at all. This is the default, and it does not look wrong in a diagram.
- The route is authenticated but ignores the answer. You set the flag, identity is established, and then your handler reaches into shared data without honouring it. The platform did its job; the route ignored it.
There is a related ordering control worth knowing by name: middleware_order, which takes "auth_first" or "middleware_first" and decides whether authentication runs before or after your own middleware. It defaults to middleware_first, meaning your middleware sees the request before auth does.
That connects directly to the auth module: the specificity ladder and the metadata-filter return value exist so that scoping is expressed once, centrally. A custom route that re-implements its own filtering by hand is quietly opting out of that, and the next lesson on authorization is where you will see exactly what it opted out of.
Try it yourself
Which config key mounts your own routes
You need a /healthz endpoint and a webhook receiver living on the same server as your agent.
{ "http": {} }
{ "ui": {} }
{ "webhooks": {} }
{ "api_version": "..." }Show answer
Correct answer: C — http, because it is the key that configures the server's HTTP layer
http is the key that lets you mount your own HTTP routes alongside the built-in Agent Server API. The tempting wrong answer is webhooks, because a webhook receiver is one of the most common reasons anyone wants a custom route, and webhooks is a real key sitting right there in the same family: it is about outbound notification configuration rather than about you defining inbound routes. api_version is version pinning and ui declares generative UI components, so both are real keys attached to unrelated jobs.
Why mount rather than run a sidecar
The architectural argument, which is more examinable than the syntax and also more useful.
What do you gain by mounting a custom route on the Agent Server rather than running a small separate service beside it?
Reveal answer
One deployable unit, one URL, one credential story, and one place your authorization handlers can apply. A custom route on the server shares the deployment's environment and its access to the store, and ships in the same revision as the agent code, so it cannot drift out of step with the agent it supports. A sidecar means a second thing to deploy, a second thing to secure, and a second thing that can drift. Note the precise wording on auth: mounting makes the existing auth layer available to the route, it does not apply it automatically, and that takes enable_custom_route_auth.
Custom routes and the auth boundary
This is the fact that turns custom routes from a convenience into a security question, and the honest answer is the opposite of the comfortable one.
A request arrives at a custom route you mounted. What has already happened to it before your handler runs?
Reveal answer
By default, nothing. The authentication middleware is applied to the protected routes only, which means /assistants, /threads and /runs; it is not global middleware wrapped around the whole app. A route mounted through http.app sits outside that set until you set enable_custom_route_auth: true, which defaults to false. Sitting on the same server is not the same as sitting behind the same door. Turn the flag on and identity is established before your handler runs, at which point the second risk appears: a handler that reads shared data without honouring that identity leaks across tenants while the platform is doing its job correctly.
When is a custom route decided
The http key sits in a build-time file but describes behaviour at request time. Which statement is right?
Show answer
Correct answer: A — Routes are declared at build time and served at runtime, so adding one is a new revision
The config file is the build-time contract, so declaring a route is a build-time act and adding one means cutting a new revision, exactly as any other code or config change in that file does. The tempting wrong answer is the runtime-registration one, because assistants are registered at runtime through the API and it is natural to generalise that pattern across the whole platform: assistants are the exception, not the rule, precisely because they are configuration objects rather than served code. Nothing in langgraph.json takes effect without a rebuild.