Skip to content

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.

bash
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 avo executable is a Ruby gem.
  • Node 22 or newer, the first node on your PATH or the binary AVO_CLI_NODE names. 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 ​

bash
gem install avo-cli
avo --version

2. Connect to your app ​

Log in once and every later command knows the app:

bash
avo login
# or with everything on the line
avo login --host https://admin.example.com/api --token avo_xxxx --api-version v1

It asks for three things and checks them against the app before saving them:

Prompt
What to answer
API base URLThe address avo-api answers on, mount path included: https://admin.example.com/api
API tokenThe secret shown once when the token was created. Typed masked, never printed
API versionEnter 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 ​

bash
avo schema

This 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, destroy

The 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:

bash
AVO_API_HOST=https://admin.example.com/api AVO_API_TOKEN=avo_xxxx AVO_API_VERSION=v1 avo schema
VariableFlagDefaultWhat it sets
AVO_API_HOST--hostnoneAPI base URL, mount path included, e.g. http://localhost:3000/api
AVO_API_TOKEN--tokennoneAPI token secret, sent as Authorization: Bearer
AVO_API_VERSION--api-versionv1API version segment of the URL
AVO_CLI_NODEnonenonePath 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:

bash
avo list users --host http://localhost:3000/api --token avo_yyyy

The 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:

bash
avo schema users
users 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 show

The 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:

bash
avo schema --format openapi > openapi.json               # the directory you ran it from
avo schema --format openapi > ~/Downloads/openapi.json   # your Downloads folder

It takes no resource and no --view: the document always covers everything the token may reach.

List records ​

bash
avo list users

The 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 2

On 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:

bash
avo list users --sort name --dir desc --fields id,name,email --per-page 10

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

bash
avo get users 5

The 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:

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

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

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

bash
avo delete users 5

No 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:

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

bash
avo update users 5 --data @user.json

Or pipe it in:

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

bash
avo get users 5 --fields name,email

Get 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:

bash
avo get users 5 --format json | jq .record.email
json
{"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 ​

bash
avo logout

This 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 codeMeaning
0Success
1The app refused or did not answer. The request was sent, so read the message before retrying a write
2The CLI refused the command before sending anything. Nothing was written
130Ctrl-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 Forbidden reasons: Tell the three refusals apart. token_entitlement suggests widening the token; policy suggests nothing, since another token will not help
  • Not found on every resource: usually an app without avo-api mounted and licensed
  • HTTP 400: also what an empty --data object gets

The app never answered. The request did not reach a response the app meant to send:

Message starts withCause 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 30sThe 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 mean when the typo is close), a flag the command does not take, an empty id, or --view without a resource
  • The data. --data that is not one JSON object, or names a file it cannot read, or includes a file field
  • The connection. No host or no token, a host that is not an http:// or https:// 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 login wrote that cannot be read, written, or parsed
  • The environment. avo login with no terminal to ask on, or no Node 22 or newer on PATH

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.

CommandShortcutWhat it does
avo loginSave the host, token and API version, so later commands need none
avo logoutDelete what login saved
avo schema [RESOURCE]sList the resources, the fields of one, or export the OpenAPI document
avo list RESOURCElList records, one page at a time
avo get RESOURCE IDgShow one record
avo create RESOURCEcCreate a record
avo update RESOURCE IDuUpdate a record
avo delete RESOURCE IDdDelete 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.

FlagCommandsDescription
--host <url>every requestAPI base URL, mount path included. Env: AVO_API_HOST
--token <secret>every requestAPI token secret, sent as a Bearer token. Env: AVO_API_TOKEN
--api-version <name>every requestAPI version segment of the URL. Defaults to the one login saved, then v1. Env: AVO_API_VERSION
--verboseevery requestLog each request and response line to stderr
--format table|jsonevery responseOutput 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, updateWhich fields to show, comma-separated, in that order. Columns on list, rows elsewhere
-d, --data <json|@path|->create, updateRequired. Fields to write, as one JSON object. @path reads it from a file, - from stdin
--view <name>schemaWhich 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>listPage number, starting at 1
--per-page <n>listRecords per page. Default: the app's Avo setting
--sort <field>listField to sort by
--dir asc|desclistSort direction. Needs --sort

login and logout are the two commands that take no RESOURCE:

  • login takes the connection flags and --verbose.
  • Without a terminal, --host and --token are required, and the API version falls back to v1.
  • logout takes no flags.