Whetstone.
Connecting With Your AgentCustom routes
Module 2, Lesson 415 min

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:

langgraph.json
{
  "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:

  1. 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.
  2. 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.

Practice

Try it yourself

Quiz

Which config key mounts your own routes

You need a /healthz endpoint and a webhook receiver living on the same server as your agent.

Candidate keys from the real key set
{ "http": {} }
{ "ui": {} }
{ "webhooks": {} }
{ "api_version": "..." }
  1. Aui, because anything serving a response to a browser is a UI concern
  2. Bapi_version, because adding routes changes the API surface
  3. Chttp, because it is the key that configures the server's HTTP layer
  4. Dwebhooks, because a custom route and a webhook are the same mechanism
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.

Recall

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.

Recall

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.

Quiz

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?

  1. ARoutes are declared at build time and served at runtime, so adding one is a new revision
  2. BRoutes are registered at runtime through the API, in the same way assistants are
  3. CRoutes are configured in the deployment interface and take effect without a rebuild
  4. DRoutes are resolved per request, so a config change applies to the next request
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.

Sign in to track your progress →