B4.run

Menu

Site

Essay · 7 min read

The App Router for AI Agents

Learn how B4.run uses file-system routes, typed tools, planning, skills and subagents to organize a LangGraph.js agent application.

May 19, 2026

Learn how B4.run uses filesystem routes to organize a TypeScript agent application.

If you have used the Next.js App Router, you already know the value of a predictable file tree. You find a page, its layout and related code by following the route through the project.

B4.run applies the same idea to agents. A route has an entry point, tools, state and optional features such as planning or delegation. Let's build up that structure with a customer support example.

GoalsCopy link to section: Goals

By the end of this post, we'll know how to:

  1. Define an agent route with a dynamic parameter.
  2. Add a tool using a TypeScript function.
  3. Declare the route's state and locate its tests.
  4. Add planning, skills, and a research subagent.

You don't need any previous experience building agents. In this example, an agent is a model that can request calls to functions we provide. Those functions are its tools, and the runtime executes them on its behalf.

Start with the routeCopy link to section: Start with the route

Suppose we're building an assistant that helps customers check the status of an order. Each organization using the application is a tenant.

Our route starts with one file:

text
src/app/support/[tenant]/index.ts

The folder path gives us the route id /support/[tenant]. The square brackets declare the tenant parameter.

Let's define the agent:

ts
// src/app/support/[tenant]/index.ts
import { agent } from "@b4run/sdk"
 
export default agent({
  model: "gpt-5-mini",
  systemPrompt:
    "You help customers of {tenant} check their orders. " +
    "Use lookupOrder to check an order before answering about its status.",
})

First, we select a model. Next, we provide instructions through systemPrompt. The {tenant} placeholder fills in the current tenant value.

The prompt refers to a lookupOrder tool, which we still need to implement. A prompt can ask the model to use a tool, but the application supplies the code and enforces access to the data.

Add a route-local toolCopy link to section: Add a route-local tool

Create a tools/ folder beside index.ts:

ts
// src/app/support/[tenant]/tools/lookupOrder.ts
export const description = "Look up an order by id."
 
export default async (
  input: { readonly orderId: string },
  ctx: { readonly signal: AbortSignal },
) => {
  const res = await fetch(`https://api.example.com/orders/${input.orderId}`, {
    signal: ctx.signal,
  })
 
  if (!res.ok) {
    throw new Error(`Order lookup failed with status ${res.status}`)
  }
 
  return (await res.json()) as { readonly status: string }
}

The service URL is a placeholder. Replace it with your order service and enforce authentication and tenant ownership there. A route parameter is just data the caller sent, so it can't prove who the caller is.

A few things to note about the function:

  1. description tells the model what the tool does.
  2. input.orderId is the argument supplied for the requested call. B4.run derives the model-facing input schema from this TypeScript type.
  3. ctx is runtime-provided context. Its signal lets the HTTP request observe cancellation.
  4. We check the HTTP status before returning a result.
  5. The returned object becomes the tool result available to the model.

Of course, the as expression only tells TypeScript what to expect. Nothing checks the JSON at runtime, so a production tool should validate any response it can't trust.

When a customer asks about an order, the agent requests lookupOrder with an order id. The runtime calls the function and sends the result back to the model, which explains the status to the customer.

Declare stateCopy link to section: Declare state

State is the data carried through a route's execution. We can declare application fields in a state.ts file:

ts
// src/app/support/[tenant]/state.ts
import { z } from "zod"
 
export default z.object({
  tenant: z.string(),
  orderId: z.string().optional(),
})

Here we use Zod to declare a required tenant string and an optional orderId string. B4.run accepts Standard Schema-compatible declarations and generates route state types from this file and the route path.

Why declare the shape? Other code can use the generated types instead of repeating the interface. If we change the state, TypeScript points us to the code that depends on it.

The tool input and route state serve different purposes. The tool input describes one function call. The route state describes data available during the route's execution. Adding orderId to state won't trigger an order lookup by itself.

Keep tests with the routeCopy link to section: Keep tests with the route

A route scenario test can live at:

text
src/app/support/[tenant]/run.test.ts

For our support route, useful scenarios include an existing order, a missing order and an order-service failure.

Model responses vary between calls. The testing tools support fixture-backed runs, where recorded or scripted model responses let us repeat a scenario without a live model call. The Testing agents guide explains how to test tool calls and final responses. Keeping the test near the route makes it easy to update both when the behavior changes.

Plan a longer requestCopy link to section: Plan a longer request

Checking one order may take one tool call. Investigating several orders and preparing a summary takes more steps.

A plan.md file can provide an initial checklist:

md
- [ ] Review the customer request
- [ ] Check order history
- [ ] Write a concise response

B4.run gives the agent a writeTodos tool to update the list, a todos state channel to retain it, and plan_update events that a client can display.

The checklist organizes the work and shows progress. The model can still skip or botch a step, so we still need tools, tests and application rules to check the result.

Add detailed instructions with a skillCopy link to section: Add detailed instructions with a skill

A skill is a Markdown file containing instructions the agent can load when needed. For example:

text
src/app/support/[tenant]/skills/refund-policy/SKILL.md

B4.run tells the agent which skills are available and exposes readSkill({ name }). The agent loads the refund instructions when a customer asks about a refund, so the whole policy doesn't ride along in every prompt.

A skill explains a procedure. If a refund requires authorization, enforce that in the application and its approval rules.

Delegate research to a subagentCopy link to section: Delegate research to a subagent

A subagent is another agent that handles a specific piece of work. Our support agent might ask a research specialist to find the relevant policy before writing an answer.

Place the specialist under subagents/:

ts
// src/app/support/[tenant]/subagents/research/index.ts
import { agent } from "@b4run/sdk"
 
export default agent({
  model: "gpt-5-mini",
  description: "Find policy documentation relevant to a support request.",
  systemPrompt: "Research the request and return concise findings with sources.",
})

The description helps the parent decide when to delegate. The systemPrompt instructs the child during its own run. To let it read documents, we add a document-search tool under subagents/research/tools/.

The parent gets a task({ subagent, input }) tool for calling its children. It can request work from research and use the findings in its response.

Each child has its own tool configuration and doesn't inherit the parent's route-local tools. To restrict delegation, use the parent's delegation policy. The Subagents guide explains the defaults and named rules.

Review the application structureCopy link to section: Review the application structure

With these additions, our route looks like this:

text
src/
  app/
    support/
      [tenant]/
        index.ts
        state.ts
        plan.md
        run.test.ts
        tools/
          lookupOrder.ts
        skills/
          refund-policy/
            SKILL.md
        subagents/
          research/
            index.ts
            tools/
              searchDocs.ts

We can follow a customer request through the files: the parent receives it, a tool retrieves order data and a specialist researches a policy. State, instructions and tests sit beside the behavior they describe.

The route convention also supports other entry types. Export a workflow for a deterministic async function, a graph for a LangGraph graph or a chain for a LangChain runnable. Each index.ts declares one entry shape, so you can pick the execution model and keep the same project layout.

ConclusionCopy link to section: Conclusion

In conclusion, the App Router idea gives an agent application a structure we can navigate with plain TypeScript files and folders. We started with one agent, added a tool, then gave state, tests, planning and delegation a place to live.

Start with only the pieces your application needs. To create a small project with the basic template:

bash
npm create b4-app@latest my-agents -- --template basic
cd my-agents
npm install

Use Node.js 24 or later, set your provider key as the generated environment instructions describe, and run npm run dev. The Getting Started guide covers the first run, and the Routes guide explains the complete route conventions.

Build your own agent.

Scaffold a project and walk through every file it gives you.