This episode widens the gateway logic with code: custom routing hooks and plugin-based decision logic, domain-specific actions that extend 9router, as well as reusable route modules across projects.

Episode 15 made the gateway fast and economical. But no matter how strong, YAML config still has limits: built-in routing logic doesn't always know that "requests from the premium member dashboard should be prioritized in the queue" or "answers for customer service need to insert customer data from an internal system". For cases like this, 9router opens the door to code.
Episode 16 introduces extensions: custom routing hooks that cut into the decision flow, plugins with their own decision logic, domain-specific actions that extend the gateway's capabilities, and route modules reusable across many projects. This is the moment the gateway is no longer just a receiver — it can think with your logic.
A hook is an injection point along the request lifecycle. 9router defines several points: before routing (before_route), after the route is selected (after_route), before the request is sent to the provider (before_upstream), and after the response returns (after_response). At these points you can read context, mutate metadata, or cancel the request.
hooks:
- name: prioritize-premium
event: before_route
action: augment_metadata
- name: enforce-business-hours
event: before_route
action: reject_if_closed
- name: log-cost-category
event: after_response
action: emit_logA hook implementation is code that exports a function receiving the request context:
export function prioritizePremium(ctx) {
const plan = ctx.request.metadata.plan;
if (plan === "premium") {
ctx.request.metadata.priority = "high";
}
return { action: "continue", ctx };
}Hooks that change across versions need their own version policy, just like routing policies in episode 10 — version the hooks so old decisions can still be audited.
A hook injects at one point; a plugin replaces a larger part of the logic — including route selection itself. A plugin is an installable package containing one or more functions that 9router executes at certain phases, offering capabilities not in the core.
9router plugin install @9router/plugin-latency-router
9router plugin enable latency-routerexport function decideModel(request) {
const maxLatency = request.metadata.sla_latency_ms;
if (maxLatency && maxLatency < 800) {
return { model: "gpt-4o-mini", reason: "sla_latency" };
}
return { model: "gpt-4o", reason: "default" };
}The latency-router plugin above picks a model based on the requested latency SLA — logic that makes no sense to hardcode into pure config. Each plugin runs in a sandbox: access is restricted, and if the plugin errors, the gateway falls back to the built-in decision logic instead of failing outright.
plugins:
latency-router:
version: "1.4.0"
sandbox: true
fallback: builtin
timeout: 10msThe list of active plugins and their versions can be checked with 9router plugin list.
Besides changing decisions, you can add new actions specific to your domain — things that would never exist in stock 9router. For example, an action that fills in customer data from a CRM before the prompt is sent, or an action that inserts a SQL query result into the model context.
actions:
- name: inject-customer-context
event: before_upstream
inputs:
customer_id: metadata.customer_id
output_field: context.customer_summaryThe action implementation is ordinary code that 9router calls with structured arguments:
export async function loadCustomerSummary({ customerId }) {
const res = await fetch(`https://crm.internal/customers/${customerId}`);
if (!res.ok) throw new Error("customer load failed");
return { summary: await res.text(), source: "crm" };
}The result is injected into context.customer_summary and available to that route's prompt template. Mind the security: the action calls an internal host, so make sure the host is allowlisted as discussed in episode 11, and every invocation is recorded in the tool_invocation audit from episode 14.
Warning
An action that calls an external service is a new data path. Apply timeouts, limited retries, and fallback: if the CRM is down, better to skip the context injection than to block the whole request.
Many workflows repeat across projects: customer support with guards, translation with strict formatting, or structured extraction with a schema. Route modules wrap one complete workflow — routes, guardrails, cache, actions, and policies — into a single package that can be imported into any project.
route_modules:
- name: support-handoff
version: "2.1.0"
routes:
- support-triage
includes:
- guardrails: [moderation-in, pii-scrub]
- cache: support-cache
- actions: [inject-customer-context]imports:
- module: support-handoff
version: "2.1.0"
overrides:
support-triage:
model: gpt-4o-miniModules bring consistency: other teams don't have to guess the right guard configuration. overrides allow local adjustments without changing the module code. By wrapping routes, guards, actions, and policies into one unit, you build the company's standard equipment — exactly the footing you'll use when documenting routing standards in episode 21.
Episode 16 turns 9router from pure config into a programmable platform: custom routing hooks inject logic at key points in the request flow, plugins replace large decision logic with sandboxed code, domain-specific actions extend the gateway with your business logic, and route modules wrap complete workflows for reuse across projects.
Key takeaways:
Your gateway can now think with its own business logic. In episode 17 we take it to world scale: Distributed & Multi-region Routing — edge routing, regional model selection, latency reduction for global users, and multi-region failover plus provider redundancy. See you there!