“Whatever job you end up doing, find a way to contribute to the field you graduated in.”
Problem
OpenFOAM is one of the most capable CFD solvers there is, and one of the hardest to use. An engineer can describe a case in one sentence. OpenFOAM needs it as a folder of plain text files with no schema, and the files have to agree with each other.
What you mean
“Lid-driven cavity, Re=100, 2D, incompressible, run for 10 seconds”
- Files
- 8
- Lines
- 349
- Checked before a run
- 0
Loading the case…
I first hit this during my bachelor’s thesis, then saw students in a CFD course get stuck the same way. I work in product design now, and this project is my way back into that field.
The hard part is the tooling, not the physics.
Who it’s for
Engineers who understand fluid dynamics but don’t want to become OpenFOAM configuration specialists.
Mechanical and aerospace engineers
They know the physics. They want a case running in minutes, with files they can still check.
Researchers
Quick sweeps across Reynolds number, geometry or model, without boilerplate case files for each run.
Meshing and CAD
GUI mesh generation and CAD import are a different problem, and out of scope.
Research
Before building the agent, I read four papers on LLMs for CFD: MetaOpenFOAM, Foam-Agent 2.0, ChatCFD and CFDLLMBench. All four landed on the same loop: plan, generate, run, review. The review step mattered most.
MetaOpenFOAM (arXiv 2407.21320), Foam-Agent 2.0 (arXiv 2509.18178), ChatCFD (arXiv 2506.02019), CFDLLMBench (arXiv 2509.20374).
Points lost in MetaOpenFOAM when the reviewer is removed
Files in a typical case, each depending on others
Lines across those files
The model
Every simulation goes through five steps. If a run fails, review sends it back with a fix.
Principles
Inspectable
The AI writes dictionaries, not results. Every file is open in the editor before it runs.
Real solver
No surrogate model and no predicted flow. blockMesh, foamRun and foamToVTK run in a container.
Correct, not just running
A case that converges to the wrong answer is worse than one that fails loudly.
Local
Everything runs on your machine, and the agent never gets a raw shell.
Describe
Type a case in plain English. The agent writes the case files in dependency order, so the patch names in the mesh match every boundary condition. Follow-up messages change only the files that need it.
Hover a highlighted phrase to see where it ends up.
Lid-driven cavity, Re=100, 2D, incompressible, run for 10 seconds
0/U
movingWall
{
type fixedValue;
value uniform (1 0 0);
}The lid moves at 1 m/s.
constant/physicalProperties
viscosityModel constant; nu 0.001 [m^2/s];
ν = U·L / Re = 1 × 0.1 / 100.
system/blockMeshDict
frontAndBack
{
type empty;
...
}One cell deep, front and back ignored.
system/controlDict
endTime 10; deltaT 0.005;
2,000 time steps.
system/controlDict
- application foamRun;
- solver incompressibleFluid;
- startFrom startTime;
- startTime 0;
- stopAt endTime;
- endTime 5;was 10
- deltaT 0.005;
- writeControl timeStep;
- writeInterval 200;
Inspect
The layout borrows from code editors: file tree on the left, editor in the middle, chat on the right. Files open as the agent writes them, and a Parameters tab turns the main values into fields you can edit without touching syntax.
- dimensions [0 1 -1 0 0 0 0];
- internalField uniform (0 0 0);
- boundaryField
- {
- movingWall
- {
- type fixedValue;
- value uniform (1 0 0);
- }
- fixedWalls
- {
- type noSlip;
- }
- frontAndBack
- {
- type empty;
- }
- }
system/controlDictSave
constant/physicalPropertiesSave
constant/momentumTransportSave
Run
Runs happen inside an OpenFOAM 13 container, with the log and a residual chart streaming live. Each run is saved to a history you can reopen.
- Prompt
- “Lid-driven cavity, Re=100, 2D, incompressible, run for 10 seconds”
- Files written
- 8
- Mesh
- 20 × 20 cells
- Solver
- foamRun · incompressibleFluid
- Time steps
- 2,000 in 17 s
- Final residual
- Ux 5.0e-9
- Exit
- 0
A real run: the velocity field of the cavity case, read from its output files. Particles follow the solved flow. Move the cursor through it to stir the flow and read the velocity at any point.
DoneblockMesh=0 · foamToVTK=0 · foamRun=0
- > blockMesh -case /cavity
- [blockMesh exited with code 0]
- > foamRun -case /cavity
- Courant Number mean: 0.222277 max: 0.836407
- Time = 10s
- smoothSolver: Solving for Ux, Initial residual = 4.98332e-09
- smoothSolver: Solving for Uy, Initial residual = 7.54293e-09
- DICPCG: Solving for p, Initial residual = 1.68815e-07
- ExecutionTime = 0.956191 s ClockTime = 1 s
- End
- [foamRun exited with code 0]
Time steps
From blockMesh to exit 0, on a laptop
Commands per run: blockMesh, foamToVTK, foamRun
Recover
When a run fails, the app reads the log, explains the likely cause and proposes a fix as a diff. You approve it before anything re-runs.
Solver log
Create time --> FOAM FATAL ERROR: solver not specified in the controlDict or on the command-line From function foamRun in file foamRun.C at line 95. FOAM exiting [foamRun exited with code 1]
Diagnosis
Missing solver in controlDict
foamRun needs a solver entry to know which solver module to load. This case has none.
system/controlDict - solidLibs ... + solver incompressibleFluid;
Results
Solved cases open in a 3D viewer inside the app, modelled on ParaView and ANSYS CFD-Post. A result is a pipeline: the mesh coloured by any field, then slices, clips, iso-surfaces, streamlines and vector glyphs, each with its own settings, played over every saved time.


Setup
Docker and a model are the two things that stop people on day one. A health check runs on first launch, checks Docker, the OpenFOAM image and the model login, and repairs what it can. Bring your own key or point it at a local model through Ollama.
- DockerRunning
- OpenFOAM imagemicrofluidica/openfoam:13
- Claude CLINot installed
$ npm install -g @anthropic-ai/claude-codeFix it for meI’ve done it - Model loginWaiting
Stored locally in your user config directory. Never leaves this machine except when calling the selected provider.
System design
Most of the work is here: what runs where, what the model can and can’t touch, and how a failed run gets back on track.
Architecture
Electron starts a local Node server, and the React interface talks to it over REST, with live updates as server-sent events. All the real work lives in core/, plain Node with no Electron in it, so the whole pipeline runs and is tested without a window.
Headless core
No Electron import is allowed in core/. Every module can be run and tested from a terminal.
Thin shell
Electron only starts the server, waits up to 30 seconds for it to answer, then loads the interface.
One port
The server serves the built interface and the API from localhost:3456, so the browser and the app see the same thing.
Agent loop
Generation is a tool-calling loop, not one big prompt. The model gets eight tools and works until it calls finish or reaches 16 steps. Its instructions tell it to save the last steps for blockMesh, checkMesh and a short solver run, so it tests a case before calling it done.
A cavity case, step by step, in the order the agent is told to work.
- 01
search_docs"lid-driven cavity incompressibleFluid"3 hits - 02
read_docwiki/cases/lid-driven-cavity.mdread - 03
list_case_filesempty case - 04
write_case_file0/Uwritten - 05
write_case_file0/pwritten - 06
write_case_fileconstant/physicalPropertieswritten - 07
write_case_fileconstant/momentumTransportwritten - 08
write_case_filesystem/controlDictwritten - 09
write_case_filesystem/fvSchemeswritten - 10
write_case_filesystem/fvSolutionwritten - 11
write_case_filesystem/blockMeshDictwritten - 12
run_commandblockMeshexit 0 - 13
run_commandcheckMeshMesh OK - 14
run_commandfoamRun --steps 10exit 0 - 15
finish8 files, validated
15 of 16 steps used
Case files
Every write stays inside the case folder and uses LF line endings.
- list_case_files
- read_case_file
- write_case_file
- edit_case_file
Docs
Search the OpenFOAM manual and the wiki, then read a full page.
- search_docs
- read_doc
Run
One command in the container, from the allowlist only.
- run_command
Finish
Refused while the case folder is empty.
- finish
Two engines
API keys go through the Vercel AI SDK. The Claude CLI option uses your own Claude Code login, so it works without a key.
Local models
With OpenAI-compatible models such as Ollama, files are written one at a time, which suits smaller models.
Every step streamed
Each tool call, result and file write goes to the interface as it happens.
Knowledge
The agent looks things up instead of relying on memory. A keyword index covers 48 pages of the OpenFOAM 13 manual and 18 wiki pages, split at headings. Search returns the source path, so the agent can read the whole page and cite it.
- wiki/errors/common-failures.mddeltaT Too Large (Divergence)
Manual pages, scraped from the OpenFOAM 13 user guide
Wiki pages on cases, solvers and common failures
Vector databases. The corpus is small, and exact keywords suit OpenFOAM’s vocabulary.
Sandbox
Every solver command runs in a fresh container with only the case folder mounted, and the container is removed when the command ends. Commands are checked against an allowlist in code, not in the prompt, and every argument is shell-escaped.
- blockMesh
- checkMesh
- setFields
- foamRun
- foamToVTK
- icoFoam
- simpleFoam
- pimpleFoam
The whole allowlist. Anything else is refused before a container exists.
Fresh each time
No container outlives its command, so no state leaks between runs.
Windows paths
The usual src:dst bind broke on the colon in C:\, so mounts use an explicit mount object.
Honest exit codes
The stream can end before Docker records the exit code, so the runner polls until it’s final instead of guessing.
Recovery
Recovery has two tiers. Known failures are matched by pattern and fixed by set rules, with no model call. Anything else goes to an LLM reviewer, which returns a corrected file or says it can’t identify the problem instead of guessing.
keyword UFinal is undefinedfvSolutionAdd a UFinal solver blockcannot find file …/phasePropertiesphasePropertiesCreate it for water and airUnknown patchField type "…"0/UReplace the type with noSlipNo reference cell foundfvSolutionAdd pRefCell and pRefValueCourant Number … max: > 1controlDictHalve deltaTanything elseanyLLM reviewer proposes a corrected fileThe five known errors, and everything else. Hover a row to see which path it takes.
Storage and streams
There’s no database. Each project is a folder, with append-only logs for runs and commands. Generation, runs and repairs all reach the interface as server-sent events.
Project folder
projects/82a55a15/ ├─ meta.json name, status, chat history ├─ case/ the OpenFOAM case │ ├─ 0/ constant/ system/ │ ├─ 0.05/ … 10/ time steps │ └─ VTK/ for the viewer ├─ runs.jsonl one line per run ├─ commands.jsonl every container command └─ runs/ └─ 342d3523.log full solver log
POST /run · text/event-stream
run-started {"runId":"342d3523"}
line > blockMesh -case /cavity
exit {"cmd":"blockMesh","code":0}
line Time = 0.005s
residual {"Ux":1,"Uy":0.99,"p":1}
line Time = 0.01s
…
exit {"cmd":"foamRun","code":0}
done {"status":"success"}
Event payloads are simplified here.
Plain files
An OpenFOAM case is already plain files, so the same folder opens in ParaView or a terminal. Nothing is locked inside the app.
Append-only
Runs and commands are added as new lines and never rewritten, so a crash can’t corrupt the history.
One stream per job
Closing the request cancels the job, and on a run that kills the container.
Results pipeline
After a run, foamToVTK converts every time step inside the container. The server builds a list of steps and patches, and the interface fetches each step when it’s needed. vtk.js has no reader for this format, so I wrote one.
Own parser
vtk.js has no reader for legacy unstructured VTK files, so the interface parses them itself.
Cached steps
Parsed steps are kept in memory, so scrubbing through time never reads from disk twice.
One engine
All vtk.js code sits behind one class. When WebGL drops its context, the viewer recovers instead of going blank.
Stack and release
- InterfaceElectron, Vite, React, TypeScript, Zustand, Monaco, vtk.js
- ServerNode, REST and server-sent events, dockerode
- AIVercel AI SDK for Anthropic, OpenAI, Google and OpenAI-compatible models; the Claude CLI as a fallback; FlexSearch for docs
- SolverOpenFOAM 13 (Foundation) in
microfluidica/openfoam:13 - TestsVitest, 25 test files: unit tests plus stages 0 to 5
- ReleaseGitHub Actions builds a macOS DMG and a Windows installer and publishes them to GitHub Releases
Validation
Physics is in the test suite. One stage runs the lid-driven cavity and compares the velocity along its centreline with Ghia et al. (1982), and a physics regression fails the build.
Largest gap to Ghia in the stage 4 test
Gap that fails the build
Final residual in that test
How I worked
I built it with AI coding agents, in stages. No layer started until the layer below it passed its tests. A markdown wiki of OpenFOAM knowledge and an agent change log kept every session grounded.
- Apr 11Stages 0 to 4 pass: Docker, files, mesh, solver, physics
- Apr 16Chat follow-ups, residual chart, error recovery
- Apr 19Bring your own key, Windows build
- Apr 27New interface, built in seven phases
- May 2Local models, health check on launch
- Jun 12Results viewer
- Jul 3macOS releases from CI
- Jul 6Agent validation loop
Outcome
Version 1.0.2 is out on GitHub for macOS and Windows, under the MIT licence. It’s validated on the lid-driven cavity and works best on incompressible laminar cases for now. Next, I’m sharing it with a professor for feedback.
What I learned
End to end
Every project before this was product design only. This is the first one I took from problem to shipped software: design, code, tests, packaging and release.
Systems, not screens
Most decisions were about where safety lives: in code, not the prompt; in rules for known errors, with the model only for the rest.
Correct over working
Using a benchmark as the pass gate changed what “done” meant.