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 LCELRunnable, 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:
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 RuntimeContextThe (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:
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.tsautomatically only for agent routes, so the workflow imports the schema and callsstate.parse(input)itself. That gives you typed state with its defaults filled in. RouteTools<"/hello/[tenant]">typesctx.toolswith the tools in this route'stools/directory (plus the sharedsrc/tools/).- B4.run returns whatever the workflow returns.
graphCopy link to section: graph
Export a compiled LangGraph graph as graph:
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:
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:
$ echo '{"tenant":"acme"}' | b4 run '/hello/[tenant]'
$ b4 dev # HMR + dev serverTo call a route from code, use the runs/wait and runs/stream endpoints described in Agent Protocol.