Routes

A route is a folder under src/app/, and its path becomes the endpoint. The route's index.ts exports one entry, in one of four shapes: agent, workflow, graph, or chain.

Route entryCopy link to section: Route entry

Here are the four shapes:

  • agent: a model-driven loop that calls the route's tools. The scaffold uses this one. See Agents.
  • workflow: a plain async function that you write step by step. See workflow below.
  • graph: a LangGraph graph, for branching, looping and conditional edges.
  • chain: a LangChain LCEL Runnable, for simple linear pipelines.

Pathname rulesCopy link to section: Pathname rules

Routes follow the Next.js App Router conventions:

  • (group)/: a route group, left out of the path
  • [segment]/: a dynamic segment, kept in the route id and in the generated route params
  • [...rest]/: a catch-all
  • [[...optional]]/: an optional catch-all

Here is an example tree:

text
src/app/
  (public)/
    hello/
      [tenant]/
        index.ts        ← exports agent | workflow | graph | chain
        state.ts        ← optional, route state schema
        tools/
          greet.ts      ← auto-discovered for agents and RuntimeContext

The (public)/ segment is left out of the path, so this route is /hello/[tenant].

workflowCopy link to section: workflow

A workflow is an async function. The first argument is the caller's JSON input, and the second is a RuntimeContext with the route's tools:

src/app/(public)/hello/[tenant]/index.ts
import type { RuntimeContext } from "@b4run/sdk"
import type { RouteTools } from "b4:routes"
import state from "./state.js"
 
export async function workflow(
  input: unknown,
  ctx: RuntimeContext<RouteTools<"/hello/[tenant]">>,
) {
  const parsed = state.parse(input)
  const r = await ctx.tools.greet({ tenant: parsed.tenant })
  return { ...parsed, greeting: r.greeting }
}

Let's quickly review:

  • B4.run passes the input to a workflow as-is. It applies state.ts automatically only for agent routes, so the workflow imports the schema and calls state.parse(input) itself. That gives you typed state with its defaults filled in.
  • RouteTools<"/hello/[tenant]"> types ctx.tools with the tools in this route's tools/ directory (plus the shared src/tools/).
  • B4.run returns whatever the workflow returns.

graphCopy link to section: graph

Export a compiled LangGraph graph as graph:

src/app/(public)/hello/[tenant]/index.ts
import { Annotation, StateGraph, START, END } from "@langchain/langgraph"
 
const HelloState = Annotation.Root({
  name: Annotation<string>,
  greeting: Annotation<string>,
})
 
export const graph = new StateGraph(HelloState)
  .addNode("greet", async (state) => ({ greeting: `Hello, ${state.name}!` }))
  .addEdge(START, "greet")
  .addEdge("greet", END)
  .compile()

The graph gets the caller's JSON input and runs in B4.run's runtime. A graph declares its own state with LangGraph's Annotation, and state.ts doesn't apply to it.

chainCopy link to section: chain

Export a LangChain LCEL Runnable as chain:

src/app/(public)/hello/[tenant]/index.ts
import { RunnableSequence } from "@langchain/core/runnables"
 
export const chain = RunnableSequence.from([
  // chain steps
])

Running a routeCopy link to section: Running a route

Run a route by its path:

bash
$ echo '{"tenant":"acme"}' | b4 run '/hello/[tenant]'
$ b4 dev    # HMR + dev server

To call a route from code, use the runs/wait and runs/stream endpoints described in Agent Protocol.