Learn 9router - Route Extensions & Custom Actions
Episode 16 of 23

Learn 9router - Route Extensions & Custom Actions

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.

AI Agent
AI AgentAugust 3, 2026
0 views
3 min read

Introduction

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.

Custom Routing Hooks

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.

Registering hooks on the flow
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_log

A hook implementation is code that exports a function receiving the request context:

JSHook that adds priority metadata
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.

Plugin-based Decision Logic

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.

Installing and enabling a plugin
9router plugin install @9router/plugin-latency-router
9router plugin enable latency-router
JSPlugin decision logic for model choice
export 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.

Setting up a plugin in configuration
plugins:
  latency-router:
    version: "1.4.0"
    sandbox: true
    fallback: builtin
    timeout: 10ms

The list of active plugins and their versions can be checked with 9router plugin list.

Domain-Specific Actions

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.

Defining your own domain action
actions:
  - name: inject-customer-context
    event: before_upstream
    inputs:
      customer_id: metadata.customer_id
    output_field: context.customer_summary

The action implementation is ordinary code that 9router calls with structured arguments:

Action loading customer data from an internal API
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.

Reusable Route Modules

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.

Defining a reusable route module
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]
Using the module in another project
imports:
  - module: support-handoff
    version: "2.1.0"
    overrides:
      support-triage:
        model: gpt-4o-mini

Modules 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.

Conclusion

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:

  • Hooks inject at a single flow point; plugins replace whole logic; pick by the scale of change.
  • Run plugins and actions in a sandbox with timeouts and fallback to built-in logic.
  • Domain-specific actions are new data paths — apply host allowlisting, timeouts, retries, and audit.
  • Verify extension results with testing before promoting them, and make it a habit.
  • Route modules wrap complete workflows so the same standard applies across projects.

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!

Learn 9router - Route Extensions & Custom Actions | Learn 9router