Command line client
avo-cli is the command line client for the REST API. It talks to an app running avo-api over HTTP, so nothing is installed in the app. Every command prints a plain text table, or the raw JSON body under --format json.
avo login # once: host, token, API version
avo schema # what the token may reach
avo list users --sort name --dir desc # a page of records
avo create users --data '{"name": "Ada", "email": "ada@example.com"}'Every line has the same shape, avo <verb> <resource> [id] [flags], with the resource named by its route key. Once you have logged in, no command needs a host or a token again.
The CLI installs without a license, the API is an add-on
avo-cli is a separate gem on rubygems.org: no license key and no gem server credentials to install it. The app it talks to must run the paid avo-api add-on. See the add-on page.
Requirements
- Ruby 3.0 or newer: the
avoexecutable is a Ruby gem. - Node 22 or newer, the first
nodeon yourPATHor the binaryAVO_CLI_NODEnames. The gem hands the line over to it. - An app running
avo-api, reachable over HTTP, and an API token created in that app.
Installation
1. Install the gem
gem install avo-cli
avo --version2. Connect to your app
Log in once and every later command knows the app:
avo login
# or with everything on the line
avo login --host https://admin.example.com/api --token avo_xxxx --api-version v1It asks for three things and checks them against the app before saving them:
Prompt | What to answer |
|---|---|
API base URL | The address avo-api answers on, mount path included: https://admin.example.com/api |
API token | The secret shown once when the token was created. Typed masked, never printed |
API version | Enter takes v1. Change it only when the app generated its controllers under another |
If the check fails, nothing is saved.
Only a flag on the login line skips a prompt. AVO_API_HOST, AVO_API_TOKEN and AVO_API_VERSION set in your shell are ignored by login, so it always asks for what it is about to save.
The token is saved as plain text
avo login writes ~/.config/avo/config.json, or $XDG_CONFIG_HOME/avo/config.json when that variable is set, %LOCALAPPDATA%\avo\config.json on Windows, and the directory AVO_CONFIG_DIR names over all of these. The Saved to line prints the path it used. Use a token you can revoke from the API tokens screen, and run avo logout on a machine you are leaving.
3. Check what the token reaches
avo schemaThis is the right first command after logging in. It lists every resource the API serves and the actions this token may call on each:
route key name entitlements
posts Post index, show, create, update, destroy
users User index, show, create, update, destroyThe first column is the name every other command takes.
Connect without logging in
If you would rather not save anything, or you are running in a job, set the variables instead and skip avo login:
AVO_API_HOST=https://admin.example.com/api AVO_API_TOKEN=avo_xxxx AVO_API_VERSION=v1 avo schema| Variable | Flag | Default | What it sets |
|---|---|---|---|
AVO_API_HOST | --host | none | API base URL, mount path included, e.g. http://localhost:3000/api |
AVO_API_TOKEN | --token | none | API token secret, sent as Authorization: Bearer |
AVO_API_VERSION | --api-version | v1 | API version segment of the URL |
AVO_CLI_NODE | none | none | Path to the Node binary the avo executable runs, instead of PATH |
A flag beats the variable, the variable beats the saved login. To reach one other app for one command, pass both halves:
avo list users --host http://localhost:3000/api --token avo_yyyyThe saved host and token are used only together
Name a host or a token yourself and the saved pair is left out entirely, so a saved token never reaches another host.
The host is the API base URL: the address the API answers on, mount path included. The CLI appends only /resources/<version>/<path> to it, so --host https://admin.example.com/api makes its requests to https://admin.example.com/api/resources/v1/.... Where an app mounts the API is Mount the API. It must start with http:// or https://.
Look up what you can send
Ask for one resource's fields before writing to it. --view create is the default:
avo schema usersusers create view
field id field type field options
name text {"required":true}
email text {"required":true}
active boolean {}
role select {"options":["user","admin","moderator"]}
team_id belongs_to {}
Try: avo schema users --view showThe first line names the resource and the view the fields belong to. field_options is one JSON object per row, printed whole so a long options list is never cut. It carries required: true on a required field (a field without it is not required), the options a choice field accepts, and multiple: true on a field that takes a list. It appears on the two form views only.
Pass --view update for what an update may send, and --view index or --view show for what a record reads back, where the field_options column is gone since nothing is sent. Each view needs the entitlement of the request it describes.
A belongs_to is listed by the key that sets it, team_id.
Export an OpenAPI document
--format openapi prints the whole API as one OpenAPI document, for the token you are logged in with. Redirect it into a file:
avo schema --format openapi > openapi.json # the directory you ran it from
avo schema --format openapi > ~/Downloads/openapi.json # your Downloads folderIt takes no resource and no --view: the document always covers everything the token may reach.
List records
avo list usersThe server decides the columns, and the pagination comes back as a footer line that ends with the command for the next page while there is one:
id name email active
1 Ada Lovelace ada@example.com true
2 Grace Hopper grace@example.com false
Page 1 of 3 (58 records). Next: avo list users --page 2On a terminal the table is framed and fits the width: a long cell wraps inside its column, and columns past the right edge are dropped, in which case the footer says how many (4 of 15 columns; --fields picks them, --format json has all). Piped into another command or a file, every column prints at full width and nothing is cut. Narrow or reorder the columns with --fields, and page and sort with the rest:
avo list users --sort name --dir desc --fields id,name,email --per-page 10Ask for a page past the last one and the app answers with the first page instead. The CLI says so on stderr, Page 9 does not exist; showing page 1 of 3., so the footer is not read as confirming the page you asked for.
Filters are not supported yet
list cannot select records by value, because the REST API does not support filters yet. Page through the records with --format json and filter them locally, for example with jq.
Read one record
avo get users 5The record reads down the page, one field per row, and --fields name,email picks the rows. On a terminal it is the same framed table as list, headed field and value, and a long value wraps inside its column; piped, there is no header and each value stays on its one line. create and update print the same view, after a line saying what they did: Created users 56.
Create a record
Pass the fields as one JSON object and the created record is printed back:
avo create users --data '{"name": "Ada Lovelace", "email": "ada@example.com"}'A create reads the resource's create view first, for the key the body nests under and to refuse a file field, then sends the POST. Use --verbose to see both requests.
Update a record
avo update users 5 --data '{"name": "Ada King"}'Every field you do not name is left as it is, and null clears the ones you do:
avo update users 5 --data '{"bio": null, "roles": []}'The update is partial per field, not inside a field's value. Setting a JSON column replaces the whole column, so send the whole object to keep the rest of it.
Delete a record
avo delete users 5No confirmation
delete asks nothing before sending: the line already names the record.
Send the right value for a field
--data (-d) takes one JSON object. Pass it inline, as @path to read a file, or as - to read stdin.
Each key is a field name from avo schema <resource>, and each value is written as the REST API takes it: Field value formats lists what every field type accepts and how to clear it. The CLI sends the values as they are, with one exception.
Key-value and code fields
Write a key_value or code field as a JSON object rather than the string the API takes. The CLI serializes it for you:
avo update users 5 --data '{"settings": {"theme": "dark"}}'Files
file and files fields are refused with exit 2. They need a multipart upload, which the CLI does not send.
Long or awkward values
When shell quoting gets in the way (a value that contains a quote, an object too long for one line), put the JSON in a file:
avo update users 5 --data @user.jsonOr pipe it in:
cat user.json | avo update users 5 --data -A successful write can still skip a field
The CLI sends --data as you wrote it. Apart from refusing file fields, it does not check your keys or values against the view. The app decides what to keep, and its strong params silently drop anything they do not permit, such as a misspelled field name or an array sent to a scalar field. The command still exits 0.
To confirm a write took, read the record back:
avo get users 5 --fields name,emailGet JSON for a script
--format json prints the body exactly as the server sent it, on stdout and nothing else, so a script can read it:
avo get users 5 --format json | jq .record.email{"record": {"id": 5, "name": "Ada", "email": "ada@example.com"}}Run in a terminal rather than piped, the same command prints the body indented and coloured for reading. Only the piped form is the server's bytes, so a script always reads through a pipe or a file.
--verbose logs each request and response line to stderr, so --format json --verbose still leaves only the body on stdout.
Sign out
avo logoutThis deletes what avo login saved. Anything set in your shell (AVO_API_HOST, AVO_API_TOKEN, AVO_API_VERSION) keeps working after it.
When a command fails
A failed command prints one message on stderr, nothing on stdout, and exits non-zero. The message names what to change, and a 401 also says where to look:
› Error: Unauthorized: http://localhost:3000 rejected the token. Check
› --token, AVO_API_TOKEN, or run `avo login` again.Add --verbose to also see the request and response lines on stderr.
| Exit code | Meaning |
|---|---|
0 | Success |
1 | The app refused or did not answer. The request was sent, so read the message before retrying a write |
2 | The CLI refused the command before sending anything. Nothing was written |
130 | Ctrl-C at an avo login prompt. Nothing was saved |
Exit 1: the app refused or did not answer
The message is one of two kinds.
The app answered with an error. The message starts with Unauthorized, Forbidden, Not found, Failed to create ..., Failed to update ..., Failed to delete ..., HTTP 400 or Server error 5xx, and carries the app's own error and, when present, reason. These are the REST API's responses, so:
- What each status means and where to fix it: the status-code table
- The two
Forbiddenreasons: Tell the three refusals apart.token_entitlementsuggests widening the token;policysuggests nothing, since another token will not help Not foundon every resource: usually an app withoutavo-apimounted and licensedHTTP 400: also what an empty--dataobject gets
The app never answered. The request did not reach a response the app meant to send:
| Message starts with | Cause and fix |
|---|---|
... redirected to ... | Never followed, since the token would be dropped on the way. Usually http:// on an app that forces https://: use the address the message points to as the host |
Connection refused, Unknown host, Timed out after 30s | The app was not reached, or did not finish answering in 30 seconds. Check the host and that the app is running |
... did not answer with ... | A 200 whose body is not what avo-api sends. The host is probably a different app |
Exit 2: the CLI refused before sending
Nothing was sent, so nothing changed on the app. The message names what to fix, and it is one of:
- The command line. An unknown command (with a
Did you meanwhen the typo is close), a flag the command does not take, an empty id, or--viewwithout a resource - The data.
--datathat is not one JSON object, or names a file it cannot read, or includes afilefield - The connection. No host or no token, a host that is not an
http://orhttps://URL or carries a username and password, or a token with a character a header cannot carry, such as a newline - The saved login. A file
avo loginwrote that cannot be read, written, or parsed - The environment.
avo loginwith no terminal to ask on, or no Node 22 or newer onPATH
Command reference
Every line has the same shape:
avo <command> [RESOURCE] [ID] [flags]RESOURCE is a resource by its route key, as schema lists them, and ID a record id.
| Command | Shortcut | What it does |
|---|---|---|
avo login | Save the host, token and API version, so later commands need none | |
avo logout | Delete what login saved | |
avo schema [RESOURCE] | s | List the resources, the fields of one, or export the OpenAPI document |
avo list RESOURCE | l | List records, one page at a time |
avo get RESOURCE ID | g | Show one record |
avo create RESOURCE | c | Create a record |
avo update RESOURCE ID | u | Update a record |
avo delete RESOURCE ID | d | Delete a record, asking nothing first |
A shortcut goes in the command's slot: avo l users is avo list users. avo help [COMMAND] or avo <command> --help prints one command's usage, and a typo close to a command gets a Did you mean: line.
| Flag | Commands | Description |
|---|---|---|
--host <url> | every request | API base URL, mount path included. Env: AVO_API_HOST |
--token <secret> | every request | API token secret, sent as a Bearer token. Env: AVO_API_TOKEN |
--api-version <name> | every request | API version segment of the URL. Defaults to the one login saved, then v1. Env: AVO_API_VERSION |
--verbose | every request | Log each request and response line to stderr |
--format table|json | every response | Output format. Default table; json prints the body exactly as the server sent it, indented on a terminal. schema also takes openapi |
--fields <a,b,c> | list, get, create, update | Which fields to show, comma-separated, in that order. Columns on list, rows elsewhere |
-d, --data <json|@path|-> | create, update | Required. Fields to write, as one JSON object. @path reads it from a file, - from stdin |
--view <name> | schema | Which view's fields. create (default) and update list what a write may send; index and show what a record reads back. Needs a resource |
--page <n> | list | Page number, starting at 1 |
--per-page <n> | list | Records per page. Default: the app's Avo setting |
--sort <field> | list | Field to sort by |
--dir asc|desc | list | Sort direction. Needs --sort |
login and logout are the two commands that take no RESOURCE:
logintakes the connection flags and--verbose.- Without a terminal,
--hostand--tokenare required, and the API version falls back tov1. logouttakes no flags.