# CLI commands (/cli/commands)



<SurfaceStatus id="cli" />

14 commands. Each wraps [REST API](/api/generations) calls. Setup first: [CLI setup](/cli/setup).

| Command        | Starts paid work | What it does                                               |
| -------------- | ---------------- | ---------------------------------------------------------- |
| `login`        | no               | Checks a key, then saves it                                |
| `logout`       | no               | Deletes the saved key file                                 |
| `whoami`       | no               | Which key and host it will use, and whether they work      |
| `credits`      | no               | Available credits and the agent spend ceiling              |
| `capabilities` | no               | What you can generate. `show <id>` adds schema and example |
| `actors`       | no               | Actors (`act_`)                                            |
| `voices`       | no               | Voices (`voc_`)                                            |
| `estimate`     | no               | The number for `--max-credits`. Reserves nothing           |
| `generate`     | **yes**          | Starts a job, waits, saves the file                        |
| `status`       | no               | Reads one generation (`gen_`)                              |
| `batch`        | no               | Reads every variant of one submit (`bg_`)                  |
| `download`     | no               | Saves outputs to disk                                      |
| `upload`       | no               | Uploads a file, prints its `ast_` id                       |
| `workflows`    | **on `invoke`**  | Lists templates, starts and follows runs                   |

## Global options [#global-options]

Put them before or after the command.

| Flag               | Short | Default               | What it does                                               |
| ------------------ | ----- | --------------------- | ---------------------------------------------------------- |
| `--json`           |       | off                   | Full API response as JSON on stdout, signed links included |
| `--quiet`          | `-q`  | off                   | Hide progress and notes. Errors and warnings still print   |
| `--api-key <key>`  |       | none                  | Use this key over the env var and saved login              |
| `--base-url <url>` |       | `https://riffads.com` | Host. Set `https://app.riffads.com`                        |
| `--help`           | `-h`  | off                   | Help for the command                                       |

* Aliases: `ls` runs `capabilities`, `balance` runs `credits`, `get` runs `status`.
* `riffads --version`, `-v` or `riffads version` prints the version.
* Write `--json` and `--quiet` bare. `--json=true` does **not** switch output.
* Accepted forms: `--name value`, `--name=value`, `-s value`, `--no-flag`, `--` to stop parsing. No bundled short flags (`-abc`).
* Unknown options are refused, with a hint: `Unknown option "--max-credit". Did you mean --max-credits?`

## Account [#account]

### login, logout, whoami [#login-logout-whoami]

```bash title="Terminal"
riffads login [--api-key <key>] [--base-url <url>]
riffads logout
riffads whoami
```

* `login` exits 0 saved, 2 no key given, 3 key refused. stdout stays empty.
* `logout` makes no network call. Always exits 0.
* `whoami --json` adds `base_url_source`, `api_key_source` (`flag`, `env`, `file`, or `default` for the host), `credits_available` and `agent_limits`.

Details: [CLI setup](/cli/setup).

### credits [#credits]

Alias `balance`. Output goes to stdout, so `riffads credits | jq` works with `--json`.

```bash title="Terminal"
riffads credits --json | jq '{available: .credits_available, ceiling: .agent_limits.most_one_generation_may_cost, bound_by: .agent_limits.bound_by}'
```

`most_one_generation_may_cost` is the highest `--max-credits` a job can pass right now. `bound_by` names the cap that sets it.

## Browse [#browse]

Read commands share a limit of 120 requests a minute.

### capabilities [#capabilities]

Alias `ls`.

```bash title="Terminal"
riffads capabilities [show <capability-id>]
```

| Flag                | Default        | What it does                             |
| ------------------- | -------------- | ---------------------------------------- |
| `--limit <n>`       | server default | Rows per page                            |
| `--cursor <cursor>` | none           | Continue from `next_cursor`              |
| `--all`             | off            | Follow every page                        |
| `--schema`          | off            | With `show`: only the JSON config schema |
| `--example`         | off            | With `show`: only a valid example config |

```bash title="Terminal"
riffads capabilities show actor_ultra --example > brief.json
riffads capabilities show actor_ultra --schema | jq '.properties | keys'
```

* List lines: id, output kind, status, name, and `needs <plan>` when gated.
* `show` is required: `riffads capabilities actor_ultra` exits 2.
* The CLI never validates config. The server names the bad field.

Every id: [capabilities](/capabilities).

### actors and voices [#actors-and-voices]

```bash title="Terminal"
riffads actors --search casual --limit 20
riffads actors --all --json | jq -r '.actors[].actor_id'
riffads voices --language en --search warm --json | jq -r '.voices[] | "\(.voice_id)\t\(.name)"'
```

| Flag                | Short | On       | What it does                         |
| ------------------- | ----- | -------- | ------------------------------------ |
| `--search <text>`   | `-s`  | both     | Match name, description, tags        |
| `--language <code>` |       | `voices` | Filter by language, for example `en` |
| `--limit <n>`       |       | both     | Rows per page                        |
| `--cursor <cursor>` |       | both     | Continue from `next_cursor`          |
| `--all`             |       | both     | Follow every page                    |

`actors --json` adds `default_voice_id`, `default_voice_name`, `is_platform`.

## Generate [#generate]

### estimate [#estimate]

```bash title="Terminal"
riffads estimate --capability <id> [--config <json|@file|->] [--set k=v]
```

Spends nothing. Limit: 60 a minute. Takes the same `--capability`, `--config`, `--set`, `--set-string` flags as `generate`.

**stdout is a bare integer.** Details go to stderr.

```bash title="Terminal"
EST=$(riffads estimate --capability veo_31 --config @look.json)
MAX=$(( (EST * 11 + 9) / 10 ))   # ceil(estimate * 1.1)
```

`at most` on stderr means `is_ceiling` is true. `--json` gives `credits`, `is_ceiling`, `breakdown`, and `note` and `seconds` when present.

### generate [#generate-1]

```bash title="Terminal"
riffads generate --capability <id> --config <json> --max-credits <n>
```

| Flag                                          | Short | Required       | Default        | What it does                                       |
| --------------------------------------------- | ----- | -------------- | -------------- | -------------------------------------------------- |
| `--capability <id>`                           | `-c`  | yes            |                | Capability to run                                  |
| `--config <json\|@file\|->`                   |       | no             | `{}`           | Capability settings                                |
| `--set <key=value>`                           |       | no, repeatable |                | One field, coerced                                 |
| `--set-string <key=value>`                    |       | no, repeatable |                | One field, kept as a string                        |
| `--max-credits <n>`                           | `-m`  | **yes**        | none           | Most this job may cost, all variants               |
| `--variants <n>`                              |       | no             | server default | Several takes of one config, up to 4, each charged |
| `--actor <actor-id>`                          |       | no             |                | Library actor                                      |
| `--actor-image-asset <asset-id>`              |       | no             |                | Uploaded image as the face                         |
| `--voice <voice-id>`                          |       | no             |                | Voice                                              |
| `--approved-voice-generation <generation-id>` |       | no             |                | Finished `tts` generation for a talking actor      |
| `--no-wait`                                   |       | no             | off            | Print the id and exit 0                            |
| `--timeout <seconds>`                         |       | no             | 1800           | Stop waiting after this                            |
| `--output <dir>`                              | `-o`  | no             |                | Save outputs here                                  |

<Callout type="warn" title="Send ceil(estimate * 1.1) as --max-credits">
  The bare estimate is refused with `max_credits_exceeded`, exit 4. `--max-credits` has no default: leaving it out exits 2.
</Callout>

What it does:

1. Submits. The id prints to stdout first, so a broken wait still leaves you the id.
2. Waits on the server's wait endpoint. Retryable refusals during the wait are slept off, up to 5 in a row.
3. Saves outputs with `-o` as `<generation_id>-<index><ext>`. Absolute paths go to stdout, one per line.

```text title="stderr (sample)"
Veo 3.1: 484 credits held for 1 output
gen_8c31fa: completed, 1 of 1 delivered
held 484 credits, charged 462 credits (captured)
  [0] video https://files.riffads.com/org/gen_8c31fa-0.mp4 (signed link hidden: use --json or riffads download)
```

* Wait timeout: exit 5. Nothing is canceled.
* Ctrl+C stops the wait, not the job. Exit 130.
* `generate -o` has no `--force`. An existing file warns and exits 2 even if the job worked. Use `riffads download <id> -o ./out --force`.
* One job at a time per key. A second gets `submission_in_flight`, exit 5.
* `run_id` null prints `Queued, starting shortly.` Not a failure.

#### Talking actor: two generations [#talking-actor-two-generations]

Voice first. Its id drives the video.

```bash title="Terminal"
SCRIPT='Three reasons these sneakers sold out twice.'

VOICE=$(riffads generate -c tts \
  --config "{\"script\":\"$SCRIPT\"}" \
  -m "$VOICE_MAX" --voice voc_ada_en --no-wait)

riffads status "$VOICE" --wait

riffads generate -c actor_ultra \
  --config "{\"script\":\"$SCRIPT\"}" \
  -m "$ACTOR_MAX" \
  --actor act_7f2a91 --voice voc_ada_en \
  --approved-voice-generation "$VOICE" \
  --output ./out
```

* `tts` needs `--voice`.
* `actor_ultra` has no `aspect_ratio`. Output is 720p, start frame shape, up to 60 seconds of audio.

More: [talking actor](/guides/talking-actor).

### Passing a config [#passing-a-config]

`estimate` and `generate` parse config the same way.

```bash title="Terminal"
--config '{"script":"..."}'    # inline
--config @brief.json           # a file
--config -                     # stdin
```

* Must be a JSON object. Omitted: `{}`.
* `--set` runs after `--config`. Dotted paths nest: `--set voice.speed=1.2` gives `{"voice":{"speed":1.2}}`.
* `--set` coerces `true`, `false`, `null` and numbers. `--set-string` never coerces and wins on the same path.

```bash title="Terminal"
jq -n --arg s "$SCRIPT_TEXT" '{script:$s}' \
  | riffads generate -c tts --voice voc_ada_en --config - -m "$MAX"
```

## Follow and download [#follow-and-download]

### status [#status]

Alias `get`.

```bash title="Terminal"
riffads status <generation-id> [--wait] [--timeout <seconds>]
```

| Flag                  | Short | Default | What it does             |
| --------------------- | ----- | ------- | ------------------------ |
| `--wait`              | `-w`  | off     | Block until the job ends |
| `--timeout <seconds>` |       | 1800    | Stop waiting after this  |

* stdout: `queued`, `rendering`, `post_processing`, `completed` or `failed`. A canceled job reads `failed`.
* An unsettled charge prints `not settled yet`, never `0`.

### batch [#batch]

```bash title="Terminal"
riffads batch <batch-group-id> [--wait] [--timeout <seconds>]
```

Same flags as `status`. stdout: `running`, `completed`, `failed` or `partial`. Exit code is the worst variant's.

### download [#download]

```bash title="Terminal"
riffads download <generation-id> [--output <dir>] [--index <n>] [--force]
```

| Flag             | Short | Default | What it does                      |
| ---------------- | ----- | ------- | --------------------------------- |
| `--output <dir>` | `-o`  | `.`     | Target folder, created if missing |
| `--index <n>`    |       | all     | Only this output                  |
| `--force`        | `-f`  | off     | Overwrite existing files          |

```bash title="Terminal"
for f in $(riffads download gen_8c31fa -o ./out); do ffprobe "$f"; done
```

* Takes an id, never a URL. It fetches fresh signed links itself (links last 600 seconds).
* Absolute paths on stdout, one per line.
* Writes `<file>.part`, renames when complete. A file is complete or absent.
* Up to 3 attempts per call.
* Existing file without `--force`: warning, exit 2. Nothing delivered yet: exit 7.

### upload [#upload]

```bash title="Terminal"
riffads upload <file> [--content-type <type>]
```

Uploads and finalizes. Only the `ast_` id goes to stdout.

| Extensions                               | Type  |
| ---------------------------------------- | ----- |
| `.png`, `.jpg`, `.jpeg`, `.webp`, `.gif` | image |
| `.mp4`, `.mov`, `.webm`                  | video |
| `.mp3`, `.wav`, `.ogg`                   | audio |

Other extensions are refused. `--content-type` overrides the guess. Size limits: [uploads](/api/uploads).

```bash title="Terminal"
riffads generate -c actor_ultra \
  --actor-image-asset "$(riffads upload face.png)" \
  --voice voc_ada_en --approved-voice-generation "$VOICE" \
  --config @brief.json -m "$MAX"
```

Exit 0 only when the file finalizes as `usable: true`. Otherwise 2.

## Workflows [#workflows]

```bash title="Terminal"
riffads workflows <templates|invoke|run> [...]
```

| Flag                         | Short | Default                  | What it does                                        |
| ---------------------------- | ----- | ------------------------ | --------------------------------------------------- |
| `--template <key>`           |       | none                     | With `invoke`: run a template, not a saved workflow |
| `--max-credits <n>`          | `-m`  | none, required to invoke | Most the **whole run** may cost                     |
| `--input <node.field=value>` | `-i`  | none                     | One input on one node, repeatable                   |
| `--wait`                     | `-w`  | off                      | Follow until done                                   |
| `--timeout <seconds>`        |       | 3600                     | Stop following after this                           |

### workflows templates [#workflows-templates]

Lists templates: key, step count, name on stdout. Each input as the `-i` flag to pass on stderr. No subcommand runs this one.

```text title="stdout"
product_demo                 4 steps  Product demo
```

There is no list of saved workflows. Copy the workflow id from the app.

### workflows invoke [#workflows-invoke]

```bash title="Terminal"
riffads workflows invoke <workflow-id> -m <n> [-i node.field=value] [--wait]
riffads workflows invoke --template <key> -m <n> [-i node.field=value] [--wait]
```

```bash title="Terminal"
riffads workflows invoke --template product_demo \
  --max-credits 900 \
  -i script.text='new sneakers, upbeat' \
  --wait --timeout 5400
```

* Exactly one of workflow id or `--template`.
* `--input` splits on the first `=` and the last `.` before it. Values coerce `true`, `false` and numbers. Everything else is a string.
* stdout: the run id (`wfr_...`).

### workflows run [#workflows-run]

```bash title="Terminal"
riffads workflows run <workflow-run-id> [--wait] [--timeout <seconds>]
```

Reads a run once, or with `--wait` checks every 30 seconds. stdout: the run status. stderr: one line per node.

| Run state                           | Exit |
| ----------------------------------- | ---- |
| Still running at timeout            | 5    |
| Partial                             | 7    |
| Failed or canceled, nothing charged | 6    |
| Failed after some steps charged     | 7    |
| Otherwise                           | 0    |

More: [workflows](/guides/workflows).

## Output streams [#output-streams]

| Stream | Carries                                                         |
| ------ | --------------------------------------------------------------- |
| stdout | Data: ids, file paths, asset ids, status words, `--json` bodies |
| stderr | People: spinner, costs, hints, warnings, errors                 |

```bash title="Terminal"
GEN=$(riffads generate -c tts --config '{"script":"hello there"}' --voice voc_ada_en -m 40 --no-wait)
ASSET=$(riffads upload face.png)
STATE=$(riffads status "$GEN")
```

Signed links never print to stdout unless you pass `--json`.

## JSON output [#json-output]

`--json` prints the response body on stdout. Failures print too:

```json title="stdout under --json"
{
  "error": {
    "ok": false,
    "code": "max_credits_exceeded",
    "message": "This generation costs more than the max_credits you set (400 credits). This one needs 462 credits. Raise max_credits, or ask for something cheaper, such as a shorter or lower resolution render.",
    "retryable": false,
    "credits_charged": 0,
    "limit": { "bound_by": "max_credits", "limit_credits": 400, "required_credits": 462 }
  }
}
```

<Callout type="warn" title="generate --json does not pipe straight into jq">
  The id line prints before the JSON. Split submit from read instead:

  ```bash
  GEN=$(riffads generate -c tts --config @brief.json --voice voc_ada_en -m 60 --no-wait)
  riffads status "$GEN" --wait --json | jq
  ```
</Callout>

| Call                                    | JSON body                           | Has                 |
| --------------------------------------- | ----------------------------------- | ------------------- |
| `generate --no-wait --json`             | Submit body                         | `generation_ids`    |
| `generate --json`, one output, finished | Wait body plus settled `generation` | no `generation_ids` |
| `generate --json` with `--variants`     | Batch body                          | `generations`       |
| `status --json`                         | Generation body                     |                     |
| `batch --json`                          | Batch body                          | `generations`       |

`login`, `logout` and `download` print no JSON on success.

`credits_charged` is `null` until the job settles. Never read `null` as `0`.

## Exit codes [#exit-codes]

| Exit | Meaning                                   | Error codes                                                                                                                                            |
| ---- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 0    | Worked                                    |                                                                                                                                                        |
| 1    | Something broke. Tell a human             | `internal_error`                                                                                                                                       |
| 2    | Change the request                        | `invalid_config`, `capability_unavailable`, `not_found`, `moderation_blocked`, `request_blocked`                                                       |
| 3    | Fix key, scope, plan or workspace         | `not_authorized`, `no_workspace`, `connection_not_configured`, `workspace_unavailable`, `required_plan`, `read_only_connection`, `insufficient_scope`  |
| 4    | Not enough credits, or a spend cap        | `insufficient_credits`, `max_credits_exceeded`, `spend_limit_exceeded`, `not_priced`, `quota_exceeded`                                                 |
| 5    | Refused, safe to send again               | `input_not_ready`, `estimate_changed`, `pricing_unavailable`, `moderation_unavailable`, `rate_limited`, `submission_in_flight`, `provider_unavailable` |
| 6    | Job failed, nothing charged               |                                                                                                                                                        |
| 7    | Job delivered only part of what was asked |                                                                                                                                                        |
| 130  | Ctrl+C. The job was not canceled          |                                                                                                                                                        |

* 6 and 7 are job outcomes. A refused submit never exits 6 or 7, and prints no id.
* `internal_error` is `retryable: true` on the wire but exits 1, so CI does not retry an outage.
* Batches exit with the worst variant. Order, least to worst: `0 < 6 < 7 < 5 < 2 < 4 < 3 < 1`.

Meaning of each code: [errors](/reference/errors).

## Retries [#retries]

* Inside a wait, the CLI retries for you: up to 5 in a row, honouring `retry_after_seconds`, else 1, 2, 4, 8, 16 seconds.
* A submit is never retried. `rate_limited`, `submission_in_flight` and `provider_unavailable` on submit exit 5 at once.
* One job in flight per key. Use one key per parallel lane.
* Resending after the first job finished starts a second paid job.
* Timeouts cancel nothing. No render time is promised: budget generously, or `--no-wait` and follow later.

## CI script [#ci-script]

```bash title="ci-generate.sh"
#!/usr/bin/env bash
set -uo pipefail

export RIFFADS_API_KEY="${RIFFADS_API_KEY:?set the key}"
export RIFFADS_BASE_URL=https://app.riffads.com

# 1. Check key and host. Exits 3 if wrong.
riffads whoami || exit $?

# 2. Start from a valid example.
riffads capabilities show tts --example > brief.json
jq --arg s "Three reasons these sneakers sold out twice." \
   '.script = $s' brief.json > brief.tmp && mv brief.tmp brief.json
VOICE=$(riffads voices --language en --json | jq -r '.voices[0].voice_id') || exit $?

# 3. Estimate. Free.
EST=$(riffads estimate --capability tts --config @brief.json) || exit $?

# 4. max_credits: ceil(estimate * 1.1).
MAX=$(( (EST * 11 + 9) / 10 ))

# 5. Submit and save. stdout: id on line 1, then one path per line.
riffads generate \
        --capability tts \
        --config @brief.json \
        --voice "$VOICE" \
        --max-credits "$MAX" \
        --output ./out > gen.out
CODE=$?
GEN=$(head -n 1 gen.out)

case "$CODE" in
  0) echo "delivered: $GEN" ;;
  2) echo "fix the request or the content"; exit 2 ;;
  3) echo "fix the key, its scope, the plan or the workspace"; exit 3 ;;
  4) echo "top up in the app, or ask for less"; exit 4 ;;
  5) echo "retryable: sleep and send the same call again"; exit 5 ;;
  6) echo "the job failed and nothing was charged"; exit 6 ;;
  7) echo "only part of it arrived"; riffads status "$GEN"; exit 7 ;;
  *) echo "broken, tell a human"; exit 1 ;;
esac
```

* **No `set -e`.** It would exit before the `case` runs.
* **Do not capture `generate -o` in a variable.** stdout has the id, then paths. Redirect and take line 1.
* **Derive `MAX` every run.** Never hardcode it.

## Not in the CLI [#not-in-the-cli]

* No cancel, logs or publish command.
* No list of past generations or saved workflows. Keep your ids.
* No outbound webhooks.
* No workflow authoring.
* Nothing posts to Meta, TikTok, YouTube or X.
