Riot is a tech-demo of a stack and tooling for building applications in OCaml.
It is heavily opinionated, and designed from the ground up to get
out of your way and help you ship great software with agents.
It comes with a single tool for all your needs, a modern package
registry, a multi-core ready actor-model runtime, a whole new
standard library, and first-class support for agentic work.
Riot is also a tool: riot — and it's the only tool
you need in this stack.
Riot ships with agent-facing instructions, not just human
docs. The riot-ml skill tells agents how to use
the stack, which commands to prefer, where workflows live, and
how to keep package discovery grounded in pkgs.ml.
Every new Riot project created with riot init includes an .agents folder with the Riot skill already there. Start the project, then use your agent.
The local skill explains how to build, test, benchmark, run, format, fix, fuzz, and maintain Riot projects.
Riot commands prefer structured output with --json, so agents can inspect build results, diagnostics, test runs, package metadata, and registry flows without scraping prose.
The skill links deeper workflow references for testing, fuzzing, benchmarking, snapshots, and package lookup, so agents can run the right loop instead of guessing.
/llms.txt is the public discovery map for agents that find Riot from the web; inside a Riot workspace, the local skill is the starting point.
Start with agents included
A new Riot workspace already has the local agent instructions it needs.
$ riot init app
$ cd app
$ ls .agents/skills
Use structured command output
JSON output turns command results into data an agent can route, summarize, or repair from.
Riot builds packages. A package is the unit of the build, and
each package is defined by a plain riot.toml file.
Riot implements a new package-aware build system, so package boundaries, dependencies, targets, and generated artifacts all belong to the same model.
riot.toml is ordinary TOML: no S-expressions, no separate configuration language, and no hidden build folklore.
Build one package, the whole workspace, debug builds, release builds, and cross targets from the same command.
Target selection supports exact triples and broad patterns like linux.
Build everything
Build every package in the workspace with the default debug profile.
$ riot build
Build one package
Narrow the build to one package when you know the slice you are working on.
$ riot build -p std
Build release artifacts
Use the release profile when you need optimized output or want to catch release-mode differences.
$ riot build --release
Cross-compile to Linux
Select a target pattern or exact triple from the targets declared in your toolchain config.
$ riot build -x linux
riot fmt
One house style
riot fmt is a fast zero-config formatter optimized
for readability and minimizing diffs.
There is one house style, and no options. Done.
Formatting is part of the stack, not a project-by-project bikeshed.
Output should be readable first and stable enough that reviews show the actual change.
Format the workspace
Rewrite files in place. The default path is the one you use locally.
$ riot fmt
Format one path
Point the formatter at a package, directory, or file when the work is narrow.
$ riot fmt packages/std/src
Check in CI
Ask Riot to report formatting drift without rewriting files.
$ riot fmt --check --json
What the rewrite feels like
The formatter should make intent easier to read and keep the diff boring.
- let greeting name=print_endline("hello, "^name)
+ let greeting = fun name ->
+ print_endline ("hello, " ^ name)
riot fix
Linting and codemods
riot fix is an extensible linter with automated
fixes, codemods, and package-provided rules.
Riot fix should not just tell you something is wrong. When Riot knows the fix, it shows the edit and can apply it.
Packages can register lint rules and fixes, then workspaces or packages can enable those rules in riot.toml.
This is what makes upgrades smoother: the package that changed can ship the rule that helps your code move with it.
Check without editing
Use the default check mode when you want diagnostics but not file changes.
$ riot fix --check
Apply safe fixes
Let Riot perform the edits it can prove are clear package-owned replacements.
$ riot fix --apply
Explain a rule
Every rule should have a stable id and an explanation that helps you decide what to do.
$ riot fix --explain class-case-module-names
Emit JSON
Use structured diagnostics when an agent or CI job needs exact fields instead of prose.
$ riot fix --check --json
Human-readable warning
The human path still names the problem, points at the source, and gives the next action.
packages/quant/tests/fair_value_tests.ml:
[warning] class-case-module-names
Module names should use ClassCase instead of underscores
3 | module Fair_value = Quant.Fair_value
| ^^^^^^^^^^
-> Rename Fair_value to FairValue
For more information about this warning, try riot fix --explain class-case-module-names
riot test
Testing
Unit tests, property tests, snapshot tests, and fuzz cases live
under one test runner.
Use unit tests for exact behavior, property tests for broad invariants, and snapshots for generated output that needs review.
Fuzz cases declared with the test API replay under riot test, so weird inputs found later become part of the ordinary regression loop.
Package and suite filters keep the local loop tight when you only need one slice of the workspace.
Run everything
Run the whole workspace when you want the broad answer.
$ riot test
Run one package
Keep the loop tight by selecting the package you are actively changing.
$ riot test -p blink
Filter by suite or case
Narrow by substring when a failure points at one test family.
$ riot test -p blink -f retry
List without running
Inspect the available suites and cases before choosing a focused run.
$ riot test --list --json
Mixed test output
One runner can report unit, property, and fuzz-style cases in the same stream.
prop blink::client_property_tests::property: retry delay is monotonic until max ... ok (63us)
prop blink::client_property_tests::property: budget allows capacity per window ... ok (139us)
prop blink::client_property_tests::property: retryable statuses match status class ... ok (125us)
test blink::client_tests::return all statuses keeps response body ... ok (978us)
test blink::client_tests::retries retryable statuses ... ok (990us)
test blink::client_tests::failure telemetry callback ... ok (943us)
fuzz blink::client_tests::connect budget blocks ... ok (399ms)
riot fuzz
Coverage-guided fuzzing
riot fuzz runs coverage-guided fuzzing campaigns
against narrow boundaries such as parsers, codecs, protocol
handlers, and CLI parsers.
A fuzz case is an ordinary Std.Test case declared with Test.fuzz, with seeds, corpus files, and mutator hints.
Generated coverage-increasing inputs are stored under .riot/fuzzing/<package>/<suite>/<case>/corpus/ as local state.
Crashes are saved under crashes/, with captured stdout, stderr, and status in crash-artifacts/ for triage.
A real finding should become a small durable regression: an inline seed, a curated fixture, or a minimized tracked crash input.
List fuzz cases
Discover the fuzz targets in a workspace before guessing selectors.
$ riot fuzz --list --json
Run a bounded campaign
Spend ten minutes exploring one package boundary with coverage feedback.
Snapshot testing checks generated output by comparing it
against a saved expected file. When the output changes, the
test writes a new candidate so you can inspect the diff and
decide whether the change is correct.
riot snapshots is the review loop for those
candidates: approve the new expected output, reject it, or
leave it pending until you understand what changed.
Use snapshots for output where the full shape matters: formatter output, diagnostics, generated docs, parser fixtures, and text reports.
Snapshot candidates are written as pending files, so generated changes do not silently replace the expected behavior.
Interactive review lets you approve, reject, ignore, or quit without manually hunting through fixture paths.
The goal is not to bless every diff. The goal is to make expected-output changes visible, reviewable, and boring to manage.
Review interactively
Walk each pending snapshot and decide whether to approve, reject, ignore, or quit.
$ riot snapshots review
Approve a slice
Promote pending expected files after you have reviewed that package or test area.
$ riot snapshots approve packages/std
Reject a bad update
Delete generated candidates when the new output is not the intended behavior.
$ riot snapshots reject packages/std/tests/parser
riot bench
Benchmarking
riot bench runs small and large benchmarks, then
records and compares runs from the same toolchain.
Benchmark suites run through Riot, so they use the same package, profile, and toolchain model as the rest of the workspace.
--record persists a run under .riot/bench when you want history, and --compare brings previous comparable runs into the current report.
Use release profile benchmarks when the performance question is about optimized code.
Run benchmarks
Run every selected benchmark once with the suite defaults.
$ riot bench
Focus on one package
Benchmark only the package and cases that are relevant to the change.
$ riot bench -p std -f parser
Record a release run
Persist an optimized run when you want future comparisons to have a baseline.
$ riot bench -p std -f parser --release --record
Compare against history
Show previous comparable runs beside the current result.
Riot ships vendored, precompiled OCaml toolchains and
compilers for supported architectures.
A workspace declares the OCaml version and target set in ocaml-toolchain.toml.
Riot provisions and validates the toolchain under ~/.riot/toolchains, including cross-toolchain components when targets need them.
Builds, docs, tests, benches, fuzzing, and package commands use the same toolchain decision.
Declare the toolchain
The workspace records the OCaml version and target set it needs.
# ocaml-toolchain.toml
[toolchain]
version = "5.5.0-riot.1"
targets = ["aarch64-apple-darwin", "x86_64-unknown-linux-gnu"]
List project toolchains
See which host and target toolchains are present for the current project.
$ riot toolchain list
Install missing toolchains
Download and validate the toolchains the workspace asks for.
$ riot toolchain install
See what Riot publishes
Inspect the set of prebuilt OCaml toolchains available for install.
$ riot toolchain list-available
riot doc
Documentation
riot doc generates package documentation locally
for your packages and dependencies.
Documentation is written in Markdown in source comments and package docs, then generated into static pages.
Generated docs follow Riot's Jolly Roger, so package docs, API reference pages, and registry docs share one readable style.
Publishing to pkgs.ml can publish docs under docs.pkgs.ml as part of the package flow.
Generate workspace docs
Build documentation locally so you can read what users will read.
$ riot doc
Generate one package
Focus docs generation on the package you are changing.
$ riot doc -p std
Generate release docs
Use the release docs shape when preparing what will be published.
$ riot doc --all --release
Choose output and JSON
Send docs somewhere specific and expose structured progress to automation.
$ riot doc -p std --output _build/doc/std --json
riot run
Run executables
riot run runs Riot executables from your
workspace, GitHub repos, and URLs.
Run local workspace binaries without manually finding the build output path.
Disambiguate by package when a workspace has more than one runnable target.
Run remote sources for project generators, scaffolding helpers, and one-off tools.
Forward runtime arguments after -- so the command remains scriptable.
Run a local binary
Build and run a workspace executable without looking up where the artifact lives.
$ riot run app
Disambiguate by package
Choose the package when several workspace packages expose runnable binaries.
$ riot run -p web server -- --port 8080
Run a remote source
Use Riot itself to run a source-addressed tool or one-off workflow from a repo.
$ riot run https://github.com/owner/tool
List runnable targets
Expose runnable binaries as data for scripts, editors, and agents.
$ riot run --list --json
riot <pkg>:<cmd>
Package provider commands
Packages can register first-class Riot commands, so
dependencies can extend your workflow without becoming
separate global tools.
A package can declare commands in riot.toml; Riot discovers them from the workspace and runs them as riot package:command.
Riot builds the package before executing the command, so provider commands stay in sync with the package version you depend on.
That makes package-specific workflows feel native: migrations, code generation, asset compilation, schema checks, and project upgrades can all live behind Riot.
Install a provider
Adding the package brings its command surface into the workspace.
$ riot add sqlx
Run package-owned workflow
The package command feels native, but the package still owns the domain behavior.
$ riot sqlx:migrate
Forward provider args
Arguments after -- go to the provider command, so commands can stay small and composable.
$ riot sqlx:prepare -- --database app_dev
What’s possible in Riot?
Riot is for building OCaml software as one piece: tools, services,
databases, packages, experiments, and systems that need to stay
understandable while they grow.
Multi-core applications
Build actor-oriented systems on top of the runtime in
std/runtime and the actor interface exposed through
std/actor. Spawn work, supervise it, and let Riot use
the cores you have.
Command line interfaces
Build fast CLIs with structured output, clear errors, and the kind
of terminal behavior agents and humans can both drive.
Cloud and networked services
Use actors, supervision, message passing, and a practical standard
library to build services that do real IO. The std/IO
surface includes ioSlice for zero-copy operations
when bytes need to move without extra copying.
Developer tooling
Write formatters, linters, code generators, release tools,
migration scripts, and project automation with the same toolchain
they plug into.
TUI applications
Build terminal interfaces with Riot packages like
minttea and gooey: structured state,
keyboard-driven UI, and native terminal ergonomics.
Web applications
Build web surfaces with suri, including LiveView
support in suri/liveview, typed domain logic, and
agent-friendly APIs without giving up the ergonomics of OCaml.
Database-backed systems
Build typed database flows with sqlx, connect to
PostgreSQL through postgres, and use
sqlite when the right database is a local file.
Agentic workflows
Create workflows that can be run, inspected, repaired, and
repeated by agents: JSON output, stable diagnostics, clear command
boundaries, and recoverable failure modes.