Command line
Scripts and CI
Ask AI, run API actions and search from scripts, cron jobs and CI, with JSON output and exit codes.
Every headless command prints its result to standard output and its progress to standard error, so you can pipe the result into jq or a file and still see what happened. None of them opens the full-screen app.
Sign in for a script
On your own machine, oatmilk login once is enough. On a server or in CI, use an API key from Developers › API keys, with only the permissions the job needs:
export OATMILK_API_KEY=oat_live_…
export OATMILK_HOST=https://books.example.com # only for your own Oatmilk
oatmilk whoami --jsonAsk AI one question
oatmilk -p "…" and oatmilk exec "…" are the same: one Ask AI request, with the answer streamed to standard output and its steps to standard error.
oatmilk -p "Which receipts am I missing this month?"
oatmilk exec "Summarize overdue invoices" --output-format json
echo "What changed this week?" | oatmilk exec
oatmilk exec --continue "And last month?" # your latest chat
oatmilk exec --chat 7d0c… "Draft the reminder emails" # a particular chat
oatmilk exec --page /finance/transactions "What needs me here?"--output-format | Prints |
|---|---|
text (the default) | The answer, as it arrives |
json | One JSON result when the answer is finished |
stream-json | One JSON event per line: chat, step, text, input, navigate, then result |
The JSON result looks like this:
{
"chatId": "7d0c4e1a-…",
"status": "done",
"response": "Three receipts are missing: …",
"steps": [{ "callId": "…", "tool": "…", "label": "…", "status": "done" }],
"opened": []
}Approvals in scripts
Ask AI asks before it deletes, voids, grants access or emails people. In a script nobody can answer, so the command stops: status is needs_input, pending lists what it is waiting on, and the exit code is 7.
oatmilk exec "Void the duplicate invoice INV-1042" --output-format json > result.json
if [ $? -eq 7 ]; then
jq '.pending' result.json # what it is waiting on
oatmilk exec "Void the duplicate invoice INV-1042" --yes # run it again, approving
fi--yes approves every step of that request, so use it only for prompts you trust. When Ask AI asked a question instead, answer it in the same chat with oatmilk exec --chat <chatId> "…".
Run any API action
oatmilk api runs any action in the API reference with your sign-in and prints its JSON data.
oatmilk api entries.list '{"limit":5}'
oatmilk api entries.list @filters.json # input from a file
cat input.json | oatmilk api invoices.create - # input from standard input
oatmilk api invoices.void --input id=inv_… --input expectedRevision=2 --input reason="Sent twice" --yes
oatmilk api entries.list '{"limit":100}' --compact | jq 'length'--input key=valuesets one field on top of the JSON. Values that parse as JSON (numbers,true, arrays) are used as such, anda.b=1sets a nested field.- Changes get an idempotency key when the action accepts one, so a retried command doesn't act twice. Pass your own with
--idempotency-key. - Actions that delete, void, grant access or email people ask first in a terminal, and need
--yesin a script. - Reads, and changes with an idempotency key, retry up to twice through a brief outage or a rate limit.
Find an action
oatmilk actions # every action
oatmilk actions invoice # actions whose name or summary matches
oatmilk actions invoices.void # one action: what it does, its permission and its input
oatmilk actions invoice --jsonThe list comes from your Oatmilk's /api/openapi.json and is cached for a day. --refresh reads it again.
Search transactions and chats
oatmilk search uber
oatmilk search --status needs_you --limit 50 --json
oatmilk chats # your recent Ask AI chats, from the terminal and the browser
oatmilk chats show 7d0c… # one chat's messages--status is one of needs_you, working, done or review.
Exit codes
Scripts can branch on the exit code instead of parsing messages. With --json, errors are JSON too: { "error": { "code", "message", … } }.
| Code | Means |
|---|---|
| 0 | Done |
| 1 | Failed |
| 2 | Wrong usage |
| 3 | Sign-in needed |
| 4 | Not allowed |
| 5 | Not found |
| 6 | Rate limited |
| 7 | Ask AI needs your input |
| 130 | Cancelled |
Example: a weekly check in CI
Save an API key with the Read permission as a masked CI variable named OATMILK_API_KEY, then run the CLI on a schedule. In GitLab CI:
missing-receipts:
image: node:22
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
script:
- npx -y @getoatmilk/cli -p "List purchases from the last 7 days that still need a receipt, with merchant and amount." > report.md
artifacts:
paths: [report.md]Any CI service works the same way: Node.js 22, OATMILK_API_KEY from its secret store, and npx -y @getoatmilk/cli.
Example: a nightly export with cron
#!/usr/bin/env bash
set -euo pipefail
export OATMILK_API_KEY="$(cat ~/.oatmilk-key)"
oatmilk api entries.list '{"limit":200}' > "entries-$(date +%F).json"crontab -e
# 0 2 * * * /home/me/export-entries.shFor a larger export, follow Export transactions, and for changes as they happen, use webhooks instead of polling.