Build a Flight Planner

Learn how to build a VFR flight planner with subagents, planning, memory and a web UI.

This recipe starts from the navlog template. It picks up where Getting Started leaves off. If you haven't built the hello agent yet, start there.

What will we build?Copy link to section: What will we build?

The template is a VFR flight planner for a Cessna 172N at /navlog. It briefs the weather from live aviationweather.gov data (no key), sends a weather and a performance subagent out, computes the navlog in code from the 1978 172N POH tables, and files an ICAO flight plan only when the pilot asks and a person approves. A browser client, the B4.run Workbench, sits in front of it.

The unit tests and evals need no API key. Live runs need a real model and an OpenAI API key.

Create the appCopy link to section: Create the app

First, scaffold the app and install its dependencies:

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

The --template navlog flag picks the flight-planner template instead of the default. You need Node.js 24 or later and npm 11.

Look at the agentsCopy link to section: Look at the agents

The app is an npm workspace with two packages. server/ is the B4.run app, and web/ is the Workbench. The root npm install installs both.

The planning route lives in server/src/app/navlog/. Let's look at the coordinator, one of its subagents and the app config:

import { agent } from "@b4run/sdk"
 
export default agent({
  model: "gpt-5-mini",
  // A plan fans out: recall → plan → two subagents → compute → brief. That
  // legitimately exceeds LangGraph's default 25 super-steps.
  recursionLimit: 100,
  description:
    "A VFR flight planner for a Cessna 172N: briefs weather, looks up POH performance, computes the navlog in code, and files a flight plan on request.",
  tools: { deny: ["runBash"], approve: [{ tool: "fileFlightPlan", allowAlways: false }] },
  systemPrompt: `You are a VFR flight-planning assistant for a Cessna 172N. Given a request:
 
1. Start with \`recall({ query: "aircraft profile and pilot preferences" })\`. The profile holds the tail number, cruise RPM and usable fuel. Use what the pilot says in the request over the profile. For anything neither gives, do not stop to ask: use the POH defaults (usable fuel 40 gal for standard tanks, or 50 gal when the pilot says long-range tanks; "full tanks" means all usable fuel; cruise 2400 RPM) and list each default under Assumptions. When the pilot states aircraft facts the profile lacks, \`remember\` them.
2. Parse the request into departure, destination, optional waypoints, cruise altitude, departure time (UTC) and people on board. Ask only when the departure, the destination or the cruise altitude cannot be determined; ask for the departure time only when none is given. Never ask whether a clock time means the next occurrence: it does. If the pilot did not say how many people are on board, assume 1 and say so in the reply. Departure time: an ISO 8601 UTC instant such as 2026-10-06T14:00:00Z; a UTC clock time such as 1400Z, which means the next occurrence; or, when the pilot names the day, "today 1400Z" or "tomorrow 1400Z". Call \`resolveDeparture({ departure })\` with it exactly as given, keeping any day the pilot named, and use the departureUtc it returns everywhere after: for the weather subagent, for computeNavlog and under Assumptions. Never work out a date or hours ahead yourself.
3. Record the legs as todos.
4. Call \`lookupAirport\` for each airport you have not already looked up.
5. Dispatch \`task({ subagent: "weather", input: "<airports and waypoints with their coordinates, altitude, departureUtc and hoursAhead from resolveDeparture>" })\` and \`task({ subagent: "performance", input: "<airports, altitude, cruise RPM>" })\`. The weather subagent estimates the ETA from the coordinates.
6. Call \`computeNavlog\` with the waypoints, the altitude, departureUtc as the departure time, the aircraft profile, personsOnBoard and one wind entry per leg from the weather brief. Never do navigation arithmetic yourself.
7. Save the navlog with \`writeFile({ path: "reports/<departure>-<destination>.md", content: "<markdown table of the legs and totals>" })\`.
8. If a chart would help, \`renderChart({ title, series })\` with fuel remaining by checkpoint.
9. Reply with the planning answer below, and nothing else.
10. When the pilot states a durable preference or an aircraft fact, call \`remember({ data, content })\`.
11. File a flight plan only when the pilot asks; see Filing.
 
Planning answer. Short and scannable, under about 180 words, in this order:
- "Bottom line: GO|CAUTION|NO-GO — <one sentence>". Use the weather brief's verdict unless performance makes it worse: a reserve under 45 minutes (reserveOk false) is NO-GO.
- "Watch for:" up to 3 bullets, each a hazard with its altitude and its valid window relative to the flight, or "none during the flight". Leave out advisories that expire before departure or start after arrival; mention them in one trailing clause at most. If the navlog's departure or final ETA moves an advisory into or out of the flight, judge it by the navlog's times. A freezing level below the cruise altitude means cruising in below-freezing air; say so plainly.
- "Numbers:" distance; ETE, which is takeoff to landing, the navlog's total time; fuel burned, which already includes 1.1 gal for start, taxi and takeoff (say so once, and never tell the pilot to add it again); fuel at landing; reserve as hours and minutes (4:24). Write plain numbers with units, never tool field names such as reserveOk. Cite the POH cruise figure once inline, as [poh/<file>.md, Figure N].
- "Assumptions:" the departure instant in UTC with its date; people on board ("1 assumed — tell me if different" when the pilot did not say); any POH default you used for usable fuel or cruise RPM; and, when the weather brief says so, that the forecast is preliminary.
- One closing line offering to file the plan, try another altitude, or re-brief closer to departure.
The Workbench shows the navlog table and the weather, so never repeat the leg table.
 
Filing. When the pilot asks to file, call \`fileFlightPlan({ flightPlan })\` once with the flightPlan from the latest computeNavlog. The runtime asks the pilot to approve that call; never ask for approval yourself. After it returns, reply in 2 to 4 lines:
- "Recorded the flight plan for <tail number> <departure>→<destination>, departing <HHMM>Z <date>. This demo records it in the workspace; it does not transmit to Flight Service."
- One line reading the key items correctly: item 15 is cruise speed, level and route (N0109VFR DCT is 109 kt true airspeed, VFR, direct), and item 16 is the destination and the total estimated elapsed time (KDLH0125 is KDLH, 1 h 25 min).
Never ask for approval again after the tool ran, and never say the plan was or will be transmitted. If the pilot denied the call, say the plan was not recorded.
 
Never write tool-call syntax such as recall({...}), a to-do list or its statuses, a list of what you ran, workspace file paths, or a citations section in a reply.`,
})

The coordinator plans the flight, sends the weather and performance subagents out, and calls computeNavlog for every number on the navlog. The tools are shared in server/src/tools/, and each subagent's tools scope keeps it to its own. fileFlightPlan asks a person before each call. The config keeps tool output inline up to 12,000 characters and makes new memories wait for review. Threads persist to SQLite by default, so they survive a restart.

A few more files shape the agent:

text
server/src/app/navlog/
  plan.md                       # seeds each thread's todo list
  memory.md                     # route-specific prompt guidance
  memory.ts                     # typed cross-session memory
  subagents/weather/            # METARs, TAFs, winds aloft, advisories
  subagents/performance/        # POH tables for the actual fields
  skills/brief-weather/         # loaded on demand: go/no-go brief
  skills/poh-lookup/            # loaded on demand: reading and citing the POH
  evals/navlog-quality.eval.ts
server/src/tools/
  computeNavlog.ts              # the navlog, computed in code
  fileFlightPlan.ts             # records an ICAO flight plan, after approval
  getMetar.ts getTaf.ts getWindsAloft.ts getAdvisories.ts lookupAirport.ts
  resolveDeparture.ts           # turns "tomorrow 1500Z" into one UTC instant
  readDoc.ts                    # reads a POH table or regulation excerpt
  renderChart.ts                # draws a bar chart the user sees
server/src/lib/                 # the math, POH tables and parsers the tools call
server/workspace/
  AGENTS.md                     # prompt guidance injected every turn
  poh/ regs/                    # the POH tables and regulation excerpts
  • plan.md turns on planning. Its checklist seeds each thread's todos.
  • workspace/AGENTS.md and the route's memory.md add persistent prompt guidance.
  • memory.ts defines typed memory records that the agent reads with recall and writes with remember.
  • The agent loads skills under skills/ by name when it needs them.

Verify and testCopy link to section: Verify and test

Run these from the workspace root. Each script delegates to the package that owns it:

bash
npm run typegen
npm run check
npm run typecheck

typegen writes server/.b4/b4.generated.d.ts. check validates routes, tools and configuration without writing files:

text
B4.run app is valid: 3 routes discovered.
- /navlog (agent)
- /navlog/subagents/performance (agent)
- /navlog/subagents/weather (agent)

Next, run both packages' tests:

bash
npm test

The server tests are unit tests of the navlog math, the POH tables, the weather parsers and the tools, with fetch stubbed. The web tests cover the Workbench. Neither needs an API key.

Then run the quality eval:

bash
npm run eval

The eval plans three flights. Its scorers require both subagents, a well-formed navlog whose totals add up with a legal reserve, a POH citation, no filing unless asked, and a passing quality grade from a model judge. Scripted fixtures cover the agent and the judge, so the eval runs without an API key; the weather tools still reach aviationweather.gov.

Run it liveCopy link to section: Run it live

These fixture-backed runs prove the app works without a key. To plan a new flight, we need a real model.

Copy the server's environment example, add your API key, run the preflight and start the dev server:

bash
cp server/.env.example server/.env
# Add your OPENAI_API_KEY to server/.env
npm run verify
npm run dev:server

verify checks the app, its types, its dependencies, Node and the provider environment. The dev server listens on http://127.0.0.1:3002 and serves Agent Protocol and AG-UI.

In a second terminal, ask for a flight:

bash
cd server
echo '{"messages":[{"role":"user","content":"Plan a VFR flight from KSTP to KRST at 4500 feet, departing 1400Z, in N738ZU (172N, 2400 RPM, 50 gal usable), and save the navlog."}]}' | npx b4 run /navlog --url http://127.0.0.1:3002

The navlog lands in server/workspace/reports/. Thread state is saved to server/.b4/checkpoints.sqlite, so threads survive dev-server restarts.

See it in a UICopy link to section: See it in a UI

The Workbench is a chat UI over the same agent. Leave the server running and start it in another terminal:

bash
npm run dev:web

Open http://localhost:3010 and choose Plan a flight. You'll see the plan card and the weather and performance subagent cards update as the agent works, tool cards, the flight-plan approval and a panel for reviewing memory. The Workbench talks to B4.run over AG-UI and holds no model credentials. Those stay in server/.env.

To build your own client, follow Flight Planner Web UI.

The scaffold also installs the Inspector. Open it in a third terminal to review the records the agent saves with remember:

bash
npx b4 inspect --cwd server

When you're ready to ship, compare Deployment Options and follow Node and Docker for the default self-hosted path.