# Scripts and CI

> Ask AI, run API actions and search from scripts, cron jobs and CI, with JSON output and exit codes.

Source: https://app.getoatmilk.com/docs/terminal/scripts

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](https://app.getoatmilk.com/docs/authentication.md#api-keys) from **Developers › API keys**, with only the permissions the job needs:

```bash
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.

```bash
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:

```json title="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`.

```bash
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](https://app.getoatmilk.com/docs/api.md) with your sign-in and prints its JSON `data`.

```bash
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](https://app.getoatmilk.com/docs/idempotency.md) 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

```bash
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

```bash
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:

```yaml title=".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

```bash title="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"
```

```bash
crontab -e
# 0 2 * * * /home/me/export-entries.sh
```

For a larger export, follow [Export transactions](https://app.getoatmilk.com/docs/tutorials/export-transactions.md), and for changes as they happen, use [webhooks](https://app.getoatmilk.com/docs/webhooks.md) instead of polling.
