There are many OCaml stacks. This one is mine.
The OCaml stack that gets out of your way.
Riot is one CLI for building, testing, and shipping OCaml, with a multicore actor runtime, a new standard library, and the pkgs.ml registry. Opinionated by design, and built to be driven by you or your agents.
macOS and Linux. Installs to ~/.riot/bin.
One CLI
Build, test, format, lint, fuzz, benchmark, document, and publish with riot.
Actor runtime
A multicore actor runtime with supervision and message passing, built in.
New standard library
A modern, practical standard library designed for systems and services.
pkgs.ml registry
Search, add, and publish packages, with docs generated for every release.
riot --help
One tool for the whole loop
From a fresh workspace to a published package, every step is a riot
command with the same flags, the same toolchain, and structured output when you need it.
Start on the golden path
riot init creates a workspace and riot new adds packages to it, so you never assemble boilerplate by hand.
- A new workspace comes with manifests, an OCaml toolchain config, and agent instructions in
.agents. riot newdrops a package into the right place without you remembering the directory layout.- Your first project already has a package, a test loop, and an obvious next command.
Create a workspace
Start from the shape Riot expects.
$ riot init app
$ cd appAdd a package
Keep new packages on the same path.
$ riot new packages/apiWhat you get
Manifests, sources, tests, and toolchains all have an obvious home.
app/
ocaml-toolchain.toml
riot.toml
packages/
app/
riot.toml
src/
tests/OCaml toolchains, handled
Riot ships prebuilt OCaml toolchains for supported platforms and installs exactly the one your workspace asks for.
- Declare the OCaml version and targets once, in
ocaml-toolchain.toml. - Riot provisions and validates toolchains under
~/.riot/toolchains, including cross-compilation targets. - Builds, tests, docs, benchmarks, and fuzzing all use the same toolchain.
Declare the toolchain
The workspace records what it needs.
# ocaml-toolchain.toml
[toolchain]
version = "5.5.0-riot.1"
targets = ["aarch64-apple-darwin", "x86_64-unknown-linux-gnu"]Install what's missing
Download and validate the declared toolchains.
$ riot toolchain installSee what's available
List every prebuilt toolchain Riot publishes.
$ riot toolchain list-availableFast, package-aware builds
Packages are the unit of the build, and each one is described by a plain riot.toml. No S-expressions, no build folklore.
- Dependencies, targets, and generated artifacts all live in one package model.
- Build one package or the whole workspace, in debug or release, from the same command.
- Cross-compile with an exact target triple or a broad pattern like
linux.
Build everything
Every package in the workspace, debug profile.
$ riot buildBuild one package
Narrow the build to the slice you're working on.
$ riot build -p stdRelease and cross builds
Optimized output, or a Linux binary from your laptop.
$ riot build --release
$ riot build -x linuxRun anything, from anywhere
riot run builds and runs executables from your workspace, a GitHub repository, or a URL.
- No hunting for build output paths: name the binary and go.
- Pick a package when more than one target is runnable.
- Pass runtime arguments after
--, so it stays scriptable.
Run a workspace binary
Build and run in one step.
$ riot run appPick the package
Disambiguate and forward arguments.
$ riot run -p web server -- --port 8080Run a remote tool
One-off tools and generators, straight from a repo.
$ riot run https://github.com/owner/toolPackages can ship commands
Dependencies can register first-class Riot commands, so migrations, codegen, and upgrades feel native instead of needing another global tool.
- Packages declare commands in
riot.toml; Riot runs them asriot package:command. - Riot builds the provider first, so the command always matches the version you depend on.
- Arguments after
--go straight to the provider.
Add a provider
Its commands arrive with the dependency.
$ riot add sqlxRun its workflow
Feels built in; the package still owns the behavior.
$ riot sqlx:migrate
$ riot sqlx:prepare -- --database app_devOne house style, zero config
riot fmt is a fast formatter tuned for readability and small diffs. There are no options to argue about.
- Formatting is part of the stack, not a per-project debate.
- Output reads well and stays stable, so reviews show the real change.
- Check mode reports drift in CI without touching files.
Format the workspace
Rewrite files in place.
$ riot fmtCheck in CI
Report drift as JSON, change nothing.
$ riot fmt --check --jsonWhat a rewrite looks like
Clearer intent, boring diff.
- let greeting name=print_endline("hello, "^name)
+ let greeting = fun name ->
+ print_endline ("hello, " ^ name)Lints that fix themselves
riot fix is an extensible linter. When Riot knows the fix, it shows the edit and can apply it for you.
- Packages can ship their own rules and fixes, and workspaces opt in from
riot.toml. - Upgrades get easier: the package that changed can ship the codemod that moves your code along.
- Every rule has a stable id and an explanation.
Check, then apply
See diagnostics first, then let Riot make the safe edits.
$ riot fix --check
$ riot fix --applyExplain a rule
Understand a warning before acting on it.
$ riot fix --explain class-case-module-namesReadable warnings
The problem, the source, and the next step.
[warning] class-case-module-names
3 | module Fair_value = Quant.Fair_value
| ^^^^^^^^^^
-> Rename Fair_value to FairValueEvery kind of test, one runner
Unit tests, property tests, snapshot tests, and fuzz cases all run under riot test.
- Unit tests for exact behavior, properties for invariants, snapshots for output that needs review.
- Fuzz cases replay as regular tests, so strange inputs found later stay fixed.
- Filter by package or test name to keep the loop tight.
Run a focused slice
Select a package, then narrow by name.
$ riot test -p blink -f retryList before running
Inspect suites and cases as data.
$ riot test --list --jsonMixed output
Units, properties, and fuzz cases in one stream.
prop blink::client_property_tests::retry delay is monotonic ... ok (63us)
test blink::client_tests::retries retryable statuses ... ok (990us)
fuzz blink::client_tests::connect budget blocks ... ok (399ms)Coverage-guided fuzzing, built in
Point riot fuzz at parsers, codecs, protocol handlers, and CLIs, and let coverage feedback find the edge cases.
- A fuzz case is an ordinary test declared with
Test.fuzz, with seeds and mutator hints. - Corpus and crashes live under
.riot/fuzzing, with captured output for triage. - Turn a real finding into a small, durable regression test.
Run a bounded campaign
Ten minutes of coverage-guided exploration.
$ riot fuzz -p blink -f retry --duration 10mReplay a crash
Reproduce one saved input against one case.
$ riot fuzz -p blink -f retry \
--replay .riot/fuzzing/blink/client/retry/crashes/id-000001Minimize the corpus
Drop redundant inputs so the corpus stays useful.
$ riot fuzz minimize-corpus -p blink -f retryReview snapshots, don't bless them
When generated output changes, the test writes a pending candidate. riot snapshots is where you approve or reject it.
- Ideal for formatter output, diagnostics, generated docs, and text reports.
- Candidates never silently replace the expected file.
- Interactive review lets you approve, reject, or skip without hunting for fixture paths.
Review interactively
Walk every pending snapshot.
$ riot snapshots reviewApprove or reject a slice
Act on one package or test area at a time.
$ riot snapshots approve packages/std
$ riot snapshots reject packages/std/tests/parserBenchmarks you can compare
riot bench runs micro and macro benchmarks, records results, and compares them against earlier runs on the same toolchain.
- Benchmarks use the same packages, profiles, and toolchain as everything else.
--recordsaves a run under.riot/bench;--compareshows history next to the current result.- Use the release profile when the question is about optimized code.
Record a baseline
Save an optimized run for later comparison.
$ riot bench -p std -f parser --release --recordCompare against history
The last five comparable runs, side by side.
$ riot bench -p std -f parser --compare 5A registry that's part of the tool
pkgs.ml is Riot's package registry: index, search, publishing, generated docs, and public download stats.
- Add, remove, update, publish, yank, and search without leaving
riot. - Publishing generates documentation and serves it from docs.pkgs.ml.
- Public stats show maintainers which versions people actually use.
Find and add a package
Search, then update your manifest and lockfile.
$ riot search std
$ riot add stdRefresh dependencies
Resolve the newest compatible set.
$ riot updatePublish and yank
Ship a release, or retract a bad one without rewriting history.
$ riot publish
$ riot yank [email protected]Docs from your source
riot doc generates documentation for your packages and their dependencies, written in Markdown right in your source comments.
- Read locally exactly what your users will read once you publish.
- Package docs and API references share one readable style.
- Published packages get their docs hosted on docs.pkgs.ml.
Generate docs
For the workspace, or one package.
$ riot doc
$ riot doc -p stdRelease docs
Preview what will be published.
$ riot doc --all --release.agents/skills/riot-ml
Agents are first-class users
Riot ships instructions for agents alongside the docs for people, so the tools you already use know how to build, test, and ship a Riot project from the first prompt.
- Every
riot initworkspace ships with a.agentsfolder and the Riot skill already in place. - Commands support
--json, so agents read build results, diagnostics, and test runs as data instead of scraping prose. - The skill links workflows for testing, fuzzing, benchmarking, and snapshots, so agents run the right loop instead of guessing.
-
/llms.txtgives agents on the web a map of Riot and pkgs.ml.
$ riot init app && cd app
$ ls .agents/skills
riot-ml
$ riot build --json
$ riot test --json
$ riot search std --json What you can build with Riot
Tools, services, databases, and experiments, built as one codebase that stays understandable as it grows.
Multicore applications
Spawn and supervise actors with std/actor and let the runtime in std/runtime use every core you have.
Networked services
Actors, supervision, and real IO. std/IO includes ioSlice for zero-copy work when bytes need to move fast.
Web applications
Build web apps with suri, including LiveView support in suri/liveview and typed domain logic.
Database-backed systems
Typed queries with sqlx, PostgreSQL through postgres, and sqlite when a file is all you need.
Command-line tools
Fast CLIs with structured output and clear errors that people and agents can both drive.
Terminal UIs
Keyboard-driven terminal interfaces with minttea and gooey, with structured state and native ergonomics.
Developer tooling
Formatters, linters, code generators, and release tools, written with the toolchain they plug into.
Agentic workflows
Workflows agents can run, inspect, and repair: JSON output, stable diagnostics, and recoverable failures.
Start a Riot project
Install the CLI, run riot init, and you're building.