Nouli is a decision graph: it gives a decision model a memory. You send facts and a question with a fixed set of answers; the model returns a probability for each answer. Nouli records the turn in a session, and the next turn only needs the new facts.
01Install
On macOS (Apple silicon), use Homebrew:
brew install standing-desks/tap/nouli
On Linux, use the download installer:
curl -fsSL https://standing-desks.github.io/nouli/install.sh | sh
The script installs the latest published binary into ~/.local/bin after verifying its checksum. It also works on Apple silicon macOS. Linux needs glibc 2.38 or newer.
If your shell cannot find Nouli, add the installation directory to your PATH:
export PATH="$HOME/.local/bin:$PATH"
nouli --version
If you previously installed with Cargo, run cargo uninstall nouli-cli before switching so the old executable does not take precedence.
To update a Homebrew installation:
brew update
brew upgrade standing-desks/tap/nouli
For a script installation, run the download installer again. Uninstall with brew uninstall standing-desks/tap/nouli for Homebrew, or remove ~/.local/bin/nouli for the script. Your configuration and saved sessions are preserved.
See the installation guide for selecting a version, changing the installation directory, inspecting the script, and building from source.
02Configure a provider
Nouli reads ~/.nouli/config.toml. The first time you make a decision without one, Nouli creates an empty file and asks you to define a provider. For TypeSafe:
default_provider = "typesafe"
default_model = "jev-latest"
[providers.typesafe]
protocol = "typesafe"
base_url = "https://api.typesafe.ai"
Then log in once. Nouli asks for the key without showing it and keeps it in ~/.nouli/auth.toml, a file only you can read, so every command works from any directory:
nouli auth login typesafe
nouli auth list
nouli auth list shows where each provider's key comes from and never prints a key. nouli auth logout typesafe removes it.
For CI and agents, use an environment variable instead. Name it in the provider with api_key_env, and Nouli reads it from your shell or from a .env in the directory you run it in. The shell wins over .env, and both win over the stored key:
[providers.typesafe]
protocol = "typesafe"
base_url = "https://api.typesafe.ai"
api_key_env = "TYPESAFE_TOKEN"
TYPESAFE_TOKEN="your-api-key"
Use --env-file FILE to read a different file. The other supported protocols are openai-decisions and openrouter-decisions. A local Laya service uses the typesafe protocol with a loopback URL and needs a context budget, because Nouli can't look up its limits:
[providers.local]
protocol = "typesafe"
base_url = "http://localhost:8000"
[providers.local.models."english"]
context_budget_tokens = 8192
reserved_output_tokens = 128
Pick a provider or model for one call with --provider NAME and --model MODEL. Each call uses your provider account and may cost money.
03Interactive mode
The quickest way to use Nouli is a session you work in. nouli open catalog opens a session full screen, creating it if needed, and a bare nouli starts a new one. Type a fact to queue it, then ask a question with ?: the question goes to the model right away, together with the queued facts. Each question is asked once, in its own call. Earlier answers stay as they were, and the model sees them as history when it answers the next question. In this session, from the robot-picking walkthrough, a warehouse arm picks a shampoo bottle in a polybag:
$ nouli open catalog
new session "catalog" · /help for commands
catalog › Illustrative item record: SKU 30418 is a 400 ml shampoo bottle in a clear polybag, weighing 0.45 kg. At this station this month the arm picked it 112 times. Suction on the flat label area succeeded first time in 81 of those picks. Most failures happened when the suction cup sealed on loose bag film instead of the bottle.
catalog › ?grasp: Which grasp should the arm prefer for this item? [Suction on the flat label area of the bottle, Suction on any flat bag surface, A two-finger pinch on the bottle neck]
turn 1 · 1 fact · 1 question
grasp ▶ Suction on the flat label area … ███████████████████▊ 0.99
Suction on any flat bag surface 0.00
A two-finger pinch on the bottl… ▎ 0.01
catalog › /new policy
new session "policy"
policy › Illustrative station rules for the picking arm: the arm acts on its own only when its chosen action has a probability of at least 0.6; otherwise a remote operator chooses. After two failed grasps on one item, stop and request remote assistance. Never place a load whose measured weight differs from the expected item weight by more than 15%. A torn or open bag goes to the exception station, never into a shipping box.
policy › ?overweight: May the arm place a load that weighs 1.9 kg when one 0.45 kg item is expected?
turn 1 · 1 fact · 1 question
overweight yes ▋ 0.03
policy › /blend catalog policy as pick
new session "pick" from catalog + policy
pick › Task: pick one unit of SKU 30418 for order 4471 into box B-12. Tote camera: 6 items. The target is detected with confidence 0.82, lying label-down, partly under another polybagged item. The only flat surface visible on it is 3 square centimeters of loose bag film.
pick › ?action: What should the arm do first? [Nudge the covering item aside to expose the target's label, Attempt suction on the visible bag film, Request remote assistance now]
turn 1 · 1 fact · 1 question
action ▶ Nudge the covering item aside t… ████████████████▊ 0.84
Attempt suction on the visible … ▋ 0.03
Request remote assistance now ██▋ 0.13
pick › Sensor update: the nudge worked and the target now lies label-up with a 6 square centimeter flat label area. Suction attempt 1 on the label lost vacuum at 2 cm of lift, because the bag film peeled away from the bottle. The loose film now hangs over the label area. The bottle neck is exposed and clear. That is one failed grasp.
pick › ?next: What should the arm do next? [Try a two-finger pinch on the bottle neck, Retry suction on the same spot, Stop and request remote assistance]
turn 2 · 1 fact · 1 question
next ▶ Try a two-finger pinch on the b… ██████████████▋ 0.73
Retry suction on the same spot █████ 0.25
Stop and request remote assista… ▍ 0.02
pick › What-if: the arm pinches the bottle neck, and pinch attempt 2 lifts the target. The wrist force sensor reads 0.46 kg against an expected 0.45 kg, and the tote camera shows a single bottle in the gripper.
pick › /whatif ?placed: What should the arm do with the load? [Lower the load back into the tote and request remote assistance, Place the load in box B-12, "Shake the load to check for anything attached, then place it"]
(not saved) · 1 fact · 1 question
placed Lower the load back into the to… 0.00
▶ Place the load in box B-12 ██████████████████▊ 0.94
Shake the load to check for any… █▎ 0.06
pick › /quit
bye
A question is one line: an ID, a colon, the prompt, and then the answers. [a, b] makes a choice, <low, medium, high> makes a score with levels in order, and no list makes a yes/no question. Quote an answer that contains a comma.
Slash commands do the rest: /whatif ?id: prompt asks its own question with the queued facts without saving, /replace N TEXT queues a correction, /branch NAME and /blend A B as NAME start new sessions, and /help lists them all. The view only shows what the store holds. Ctrl-C clears the row, then what you have queued, and then quits, as /exit does.
A form under the transcript holds the queued facts, the question, and its options or levels, so you don't write the brackets yourself. Shift-Tab switches between yes/no, choice, and score, and Tab, Up, and Down move between the sections. The session's decisions are drawn to the right when the terminal is at least 116 columns wide, and Ctrl-G shows them in a narrower one. The lines shown above are typed into the form's first row, typing / lists the commands, and /sessions resumes an earlier session.
04Make a decision
Everything you can do at the prompt is also a command, for scripts and agents. nouli new starts a saved session. Give it the facts, then the question with -q. A question is one line: an ID, a prompt, and the options in brackets:
nouli new travel \
'The train costs 40 USD and arrives 30 minutes before the meeting.
A taxi costs 70 USD and also arrives in time. Prefer the cheaper option
that arrives on time.' \
-q 'transport: How should I travel to the meeting? [Take the train, Take a taxi]'
turn 1 · 1 fact · 1 question
transport ▶ Take the train ████████████████████ 1.00
Take a taxi 0.00
typesafe/jev-1.14.0 · session 01a188cf-5a98-7734-9645-715ee6bd9e7d · turn 01a188cf-5b18-75dc-8b15-90393e62bd69
Every option gets a probability, and ▶ marks the answer. Each question has one of three answer types:
| type | answer | how to write it |
|---|---|---|
choice | One option, with a probability for every option | id: prompt [option, option] |
noul | The probability that the answer is yes | id: prompt |
score | The expected position on an ordered rubric, which can be fractional | id: prompt <poor, fair, good> |
Put an option in double quotes if it contains a comma or a bracket. A question can sit in a file too: pass --input request.json (JSON or TOON) for the full form with rubrics and structured evidence. Answers are model judgments, not actions: Nouli never books, sends, or changes anything.
05Add a turn
A session keeps the facts you gave it and every answer. nouli add sends only what changed, with the next question, and the earlier facts and answers come along as history. Nothing is asked again, so transport keeps the answer it was given. nouli ask travel -q … asks without new facts:
nouli add travel \
'The train has been cancelled. The taxi is still available at 70 USD.' \
-q 'rebook: How should I travel to the meeting now? [Take the train, Take a taxi]'
turn 2 · 1 fact · 1 question
rebook Take the train ▎ 0.01
▶ Take a taxi ███████████████████▊ 0.99
typesafe/jev-1.14.0 · session 01a188cf-5a98-7734-9645-715ee6bd9e7d · turn 01a188cf-5caa-7258-85eb-d6c2ae37dfc0
Every turn asks one question, in its own call. Facts are numbered, and --replace 1 'new text' corrects fact 1 in the turn that asks the next question: the model sees only the new text, and the old one stays in the history. nouli whatif travel 'The taxi fare has doubled to 140 USD.' -q 'fare: Is the taxi still worth it?' asks a question of a hypothetical fact and saves nothing.
Sessions are saved under ~/.nouli and work from any directory. --store DIR uses a different store.
Long histories are trimmed to fit the model's context budget: older decisions shrink to their question and answer first, then the oldest optional records drop out. The full record stays in the store either way. nouli context SESSION shows exactly what reached the model for a turn.
06Branch and blend
--from starts a new session from saved history. One --from branches, and the branch keeps the source's questions; several blend, and a blend starts with none:
nouli new travel-late --from travel '…' -q '…'
nouli new plan --from travel --from budget '…' -q '…'
A new session captures its sources when you create it. Later turns in travel don't change plan; make a new blend to reconsider with the updated history. To start from an older point, name the turn: --from travel@TURN_ID.
07See the graph
nouli view plan
nouli view plan --mermaid > plan.mmd
nouli facts plan
nouli questions plan
nouli sessions list
nouli sessions show plan
nouli sessions export plan --format json > plan.json
nouli view draws the session and every turn it captured as boxes in your terminal, with ▶ marking each selected answer. --mermaid prints the same graph as Mermaid source; paste it into a ```mermaid block and GitHub draws it. nouli facts lists the numbered facts, marking those inherited from another session, and nouli questions prints the session's questions in the one-line form. None of these commands call a model or change the store.
08Run a workflow
When later questions depend on earlier answers, describe the turns as a graph and let Nouli run it. Nodes with no dependency between them run in parallel:
{
"state": [],
"nodes": [
{"id": "choose", "turn": {
"state": [{"type": "text", "text": "The train takes 4 hours; the flight takes 2 hours."}],
"questions": [{"id": "route", "prompt": "Which route should I take?",
"answer_space": {"type": "choice", "options": [
{"id": "train", "description": "Take the train"},
{"id": "flight", "description": "Take the flight"}]}}]}},
{"id": "book", "dependencies": ["choose"], "turn": {
"state": [],
"questions": [{"id": "book_now", "prompt": "Given the preferred route, should I book it now?",
"answer_space": {"type": "noul"}}]}}
]
}
nouli workflow run workflow.json --name review --parallel 2
nouli workflow show RUN_ID
nouli workflow resume RUN_ID
A run saves its graph and settings before it starts. If it's interrupted, resume reuses finished nodes and runs the rest. --jsonl streams progress events.
09Output and exit codes
Output is text in a terminal and JSON when piped. Choose explicitly with --format text|json|jsonl|toon, --json, or --jsonl.
| exit | meaning |
|---|---|
| 0 | Completed, including replays and per-question refusals |
| 2 | Invalid input or configuration |
| 3 | Provider, authentication, capability, contract, or capacity failure |
| 4 | Partial or failed workflow |
| 5 | Request, writer, context, or advanced-head conflict |
| 6 | Storage, internal, or output failure |
| 130 | Interrupted; recovery needed |
10Troubleshooting
| message | what to do |
|---|---|
providers_required | Open the configuration file named in the error and add a [providers.NAME] section. |
provider_required | Set default_provider or pass --provider NAME. |
model_required | Set default_model or pass --model MODEL. |
unknown_provider | Use a provider name defined in your configuration. |
credential_unavailable | Run nouli auth login PROVIDER, or set the environment variable named in the error, in your shell or the env file. |
auth_store_insecure | Others can read ~/.nouli/auth.toml. Run chmod 600 on it. |
questions_required | The turn has no question. Add one with -q. |
invalid_standing_question | A request file sets standing, which Nouli no longer supports. List the questions in turn.questions. |
env_file_not_found | Check the path passed to --env-file. |
invalid_env_file | Check assignments, quotes, and UTF-8 encoding in the env file. |
invalid_model_budget | Add context_budget_tokens under [providers.NAME.models."MODEL"] or pass --context-budget TOKENS. |
invalid_configuration_syntax | Check the TOML. Top-level settings go before provider sections. |
A provider can refuse a single question; that's recorded as a refusal, not an error. Run nouli --help for every option. The reference docs cover each subsystem in detail.