Mental Model

B4.run is a meta-framework for LangGraph. LangGraph runs the graph well. But every team we've seen build agents on LangGraph ends up writing the same things by hand: project structure, type wiring, dev tooling and deployment scripts. B4.run writes those conventions once, so you can focus on the agent.

tl;drCopy link to section: tl;dr

  • A route is a folder. Its path is the agent endpoint.
  • A route's index.ts exports one of: agent, workflow, graph, chain.
  • Tools live in the shared src/tools/ or in a route's own tools/ directory. B4.run infers their parameter types at build time.
  • State is the JSON a route takes in and returns.
  • Agent routes can turn on built-in features such as memory, planning, skills, subagents, and reasoning effort.
  • Middleware is an optional request gate for the dev server and the built HTTP runtimes.
  • LangGraph runs the graph. B4.run handles the rest.

The piecesCopy link to section: The pieces

A route is a folder under src/app/. The folder path becomes the endpoint, so src/app/navlog/ answers to /navlog. Everything a route needs lives in that folder.

A route entry is what the route runs. Each route has one index.ts that exports an agent, a workflow, a graph, or a chain. Pick the shape that fits the problem, route by route.

Export an agent when the model should decide what to do. It picks tools as it goes and can pause for a person. Export a workflow when you want to write the steps yourself as a plain async function. Both get the same routing, types and dev loop. You can drop down to graph or chain for raw LangGraph whenever you like. See Agents and Routes for each shape.

Tools are default-exported async functions in the shared src/tools/ or a route's own tools/ directory. B4.run reads each tool's parameter type at build time and turns it into a JSON schema. Every route can use the shared tools, and a route-local tool with the same name replaces the shared one for that route. In an agent, the model decides when to call each tool.

State is an optional typed shape in a state.ts file next to the route. Dynamic segments ([tenant], [...rest]) stay in the route id and the generated route metadata. Callers pass their values in the JSON input.

Agent features turn on when B4.run finds their files:

  • workspace/AGENTS.md adds prompt guidance on every turn.
  • memory.ts defines typed memory that lasts across threads, and episodic records keep run history for later recall.
  • plan.md turns on planning, skills/<name>/SKILL.md adds skills, and subagents/ holds child agents.
  • The reasoning option tunes OpenAI-backed agent routes.

Middleware runs before the route in b4 dev and in the built Node and Hono HTTP runtimes. It lives at src/middleware.ts (or a root middleware.ts). It can reject a request or attach context for the tools to read.

Access control: four controlsCopy link to section: Access control: four controls

Four separate controls decide what a running route can do:

You need all four, since each covers something the others don't. They all sit behind your service's own authentication and thread access policy. See Access Control for how they fit together.

The runtimeCopy link to section: The runtime

A typical request through B4.run looks like this:

text
POST /threads/<id>/runs/wait  { "route": "/navlog#agent", "input": { "messages": [...] } }
        │
        ▼
   thread lookup   →   .b4/threads.sqlite (created on POST /threads)
        │
        ▼
   route id match  →   src/app/navlog/index.ts
        │
        ▼
   input state     →   { messages: [{ role: "user", content: "Plan a VFR flight from KSTP to KRST at 4500 feet." }] }
        │
        ▼
   middleware       →   src/middleware.ts (auth, request context, ...)
        │
        ▼
   route entry      →   index.ts (agent | workflow | graph | chain)
        │
        ▼
   tool calls       →   readDoc({ path: "poh/cruise-performance.md" })
                         → { content: "..." }
                         computeNavlog({ waypoints: [...], altitudeFt: 4500, ... })
                         → { legs: [...], totals: { distanceNm: 66, fuelGal: 5.5, ... } }
        │
        ▼
   typed output     →   "66 nm, 33 minutes, 5.5 gal burned. [poh/cruise-performance.md, Figure 5-7]"

Let's walk through each step:

  1. Thread lookup. The Agent Protocol endpoint finds the thread in .b4/threads.sqlite. You create a thread with POST /threads and pass its id in later run requests.
  2. Route id match. B4.run matches the route id against the file tree. Route groups ((public)/) drop out of the path, and the #agent suffix picks the agent entry.
  3. Input state. B4.run passes the JSON input to the route. Agent routes fill in defaults from state.ts. Workflow, graph, and chain routes get the input as sent (a workflow can parse it with its own state.ts).
  4. Middleware. The app's middleware runs, if there is one. It can stop the request or let it through with context for the tools.
  5. Route entry. The exported agent, workflow, graph, or chain runs. Agent routes get their tools wired in, and workflow(input, ctx) gets the typed RuntimeContext. Raw graph and chain routes run the way LangGraph and LangChain run them, with the dependencies they import.
  6. Tool calls. B4.run checks each tool call against the schemas it generated at build time. Here, readDoc reads a POH table under poh/ and computeNavlog computes the legs, time and fuel in code. The model decides when to call each.
  7. Typed output. The route returns a typed value. B4.run returns it in one response (runs/wait) or streams it (runs/stream).

Runs happen on a thread. Between turns, B4.run saves the LangGraph state to SQLite in .b4/checkpoints.sqlite.

Build vs runtimeCopy link to section: Build vs runtime

b4 build and b4 typegen walk your routes, infer tool parameter types from function signatures, and write .b4/b4.generated.d.ts. That file holds the route registry, the tool registry, and the typed RouteTools<P> map. Once it exists, your editor autocompletes ctx.tools.computeNavlog and flags a caller that no longer matches a tool's signature.

At runtime, b4 run, b4 dev, and the built HTTP entries run your routes. For agent and workflow routes, you never write a tools: [...] array or register a route. B4.run wires that up from the files.

Where B4.run ends and LangGraph beginsCopy link to section: Where B4.run ends and LangGraph begins

B4.run builds on LangGraph and packages LangGraph apps for several hosts.

ComponentRole
B4.runFile-system routing, tool and state typegen, the HMR dev server, CLI commands, generated entries, and the default SQLite stores.
LangGraphStateGraph, channels, graph execution, and the checkpoint interface.
Node targetB4.run's full Agent Protocol and AG-UI HTTP server, including Node filesystem-backed capabilities.
Hono targetB4.run's fetch-based HTTP runtime for edge hosts. Node-only filesystem features aren't available.
LangSmith targetGenerated graph entries and langgraph.json for LangSmith to deploy. LangSmith provides hosting and observability.

The default @b4run/sqlite-storage package backs both the checkpointer (.b4/checkpoints.sqlite) and the thread store (.b4/threads.sqlite). You can swap in your own.

LangSmith is a deployment and observability platform, separate from LangGraph. B4.run generates the files LangSmith deploys (langgraph.json and one entry file per route). You bring your own LangSmith project and tracing setup.

You also bring your own LangGraph graphs, LCEL chains and model providers. B4.run leaves them as they are and puts conventions around them.