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:

Shell
export OATMILK_API_KEY=oat_live_…
export OATMILK_HOST=https://books.example.com   # only for your own Oatmilk
oatmilk whoami --json

Ask 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.

Shell
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-formatPrints
text (the default)The answer, as it arrives
jsonOne JSON result when the answer is finished
stream-jsonOne JSON event per line: chat, step, text, input, navigate, then result

The JSON result looks like this:

oatmilk exec … --output-format json
{
  "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.

Shell
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.

Shell
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=value sets one field on top of the JSON. Values that parse as JSON (numbers, true, arrays) are used as such, and a.b=1 sets 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 --yes in a script.
  • Reads, and changes with an idempotency key, retry up to twice through a brief outage or a rate limit.

Find an action

Shell
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 --json

The list comes from your Oatmilk's /api/openapi.json and is cached for a day. --refresh reads it again.

Search transactions and chats

Shell
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", … } }.

CodeMeans
0Done
1Failed
2Wrong usage
3Sign-in needed
4Not allowed
5Not found
6Rate limited
7Ask AI needs your input
130Cancelled

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:

.gitlab-ci.yml
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

export-entries.sh
#!/usr/bin/env bash
set -euo pipefail
export OATMILK_API_KEY="$(cat ~/.oatmilk-key)"
oatmilk api entries.list '{"limit":200}' > "entries-$(date +%F).json"
Shell
crontab -e
# 0 2 * * * /home/me/export-entries.sh

For a larger export, follow Export transactions, and for changes as they happen, use webhooks instead of polling.