Skip to main content

Scripting and automation

The CLI is built to run without anyone watching: in shell scripts, cron jobs, CI pipelines, and AI agents. This page covers what you can rely on when you do that: output formats, exit codes, errors, retries, and how to test a command safely.

stdout is data only​

Every command writes its result to stdout. Progress messages, prompts, warnings and errors go to stderr. Piping a command's output always gives you just the data.

Output formats​

Choose the format with -o (or --output, or the MIXPOST_OUTPUT environment variable).

When you don't choose one, the CLI uses table on a terminal and json everywhere else. A command in a pipe or a script gets JSON without you asking for it.

FormatOutput
tablealigned columns, for reading
jsonthe API response, unchanged
yamlthe API response, as YAML
csvthe table columns, comma-separated
tsvthe table columns, tab-separated
idone identifier per line (the uuid, or the numeric id when there's no uuid)
templateyour own Go template, set with --template
mixpost post list # table on a terminal
mixpost post list | jq -r '.data[].uuid' # JSON in a pipe
mixpost post list -o id | xargs -n1 mixpost post get
mixpost post list --all -o csv --no-headers > posts.csv
mixpost post list -o template \
--template '{{range .data}}{{.uuid}} {{.status}}{{"\n"}}{{end}}'
  • JSON is passed through unchanged, so you also get fields the CLI doesn't know about. Add --compact to print it on one line.
  • Tables are a summary. For analytics in particular, the table shows the main figures, and JSON has everything, including charts.
  • --no-headers removes the header row from table, csv and tsv.

Exit codes​

Each kind of failure has its own exit code, so a script can react to it without parsing error messages:

CodeMeaning
0success
1other failure
2wrong command, flag or configuration (for example, no workspace set)
3401 not authenticated: the key is missing, wrong or expired
4403 forbidden: your role doesn't allow this
5404 not found
6422 validation failed, including unmet platform requirements
7429 rate limited
85xx server error
9network failure or timeout
10cancelled at a confirmation prompt
mixpost post get "$uuid" > /dev/null
case $? in
0) echo "exists" ;;
5) echo "gone" ;;
*) echo "something else went wrong" >&2; exit 1 ;;
esac

Errors​

When the output format is json, yaml or template (which includes every run in a pipe), errors are written to stderr as JSON. A script or an agent can read them the same way it reads data:

{
"error": {
"message": "Validation errors",
"status": 422,
"exit_code": 6,
"errors": { "versions.0.content": ["The content is required."] }
}
}

errors lists every field that failed validation, with the reasons. With table, csv, tsv or id output, the error is printed as plain text instead.

Confirmations​

Commands that delete things ask before they act: post delete --permanent, media delete, media folder delete, tag delete, mention delete and auth logout.

When there is no terminal to ask on, these commands refuse to run instead of assuming you agree. Pass --yes (or -y) to confirm in a script:

mixpost post delete "$uuid" --permanent --yes

Test a command with --dry-run​

--dry-run prints the request the command would send (method, URL, headers and body) and stops without changing anything:

mixpost post create --text "Launch" --account linkedin --at "2026-09-10 15:00" --dry-run

Lookups still run during a dry run. Turning -w acme or --account linkedin into an id needs a read request, and that is what makes the printed request accurate.

To see every request and response of a real run, add --debug. The trace is written to stderr.

Retries and timeouts​

Requests that fail because of rate limits, network errors or server errors are retried, 2 times by default. Change this with --retries.

Requests that create something (POST) are only retried after a 429, because a 429 means the server didn't process the request. This way a retry never creates a post twice.

Each request times out after one minute. Change this with --timeout, for example --timeout 2m.

Pagination​

List commands return one page by default.

  • --all fetches every page.
  • --limit N stops after N items.
  • --page and --per-page fetch a specific page.
mixpost post list --status failed --all -o id

With mixpost raw, use --paginate or --limit to do the same for any list endpoint.

CI and cron​

You don't need a configuration file in CI. Set the key (and the address, for a self-hosted instance) as environment variables from your secrets, and pass the workspace with -w:

.github/workflows/announce.yml
name: Announce release
on:
release:
types: [published]

jobs:
announce:
runs-on: ubuntu-latest
steps:
- name: Install Mixpost CLI
run: curl -fsSL https://raw.githubusercontent.com/inovector/mixpost-cli/main/install.sh | sh

- name: Queue the announcement
env:
MIXPOST_PERSONAL_KEY: ${{ secrets.MIXPOST_PERSONAL_KEY }}
MIXPOST_URL: ${{ vars.MIXPOST_URL }} # leave empty for Mixpost Cloud
run: |
mixpost post create -w "${{ vars.MIXPOST_WORKSPACE }}" \
--text "${{ github.event.release.name }} is out: ${{ github.event.release.html_url }}" \
--account twitter --account linkedin --queue

A weekly report from cron, every Monday at 7:00:

# m h dom mon dow command
0 7 * * 1 mixpost analytics summary -w acme --period last_week -o json > ~/reports/weekly.json
Use a dedicated key

Create a separate personal key for each pipeline, with an expiry date. If one leaks, you can revoke it without affecting anything else. Keys are listed on the API page in the user menu.

Recipes​

Empty the trash:

mixpost post list --trashed --all -o id | xargs mixpost post delete --permanent --yes

Schedule a post for each account's best time:

mixpost analytics best-times --account linkedin --tz Europe/Chisinau -o json
# take the time from the output, then:
mixpost post create --text "Weekly tips" --account linkedin \
--at "2026-10-20 09:00" --tz Europe/Chisinau

Export last month's analytics to a file:

mixpost analytics summary --period last_month -o json > analytics-$(date +%Y-%m).json

AI agents​

Agents that run shell commands, such as Claude Code, Codex, Cursor or Gemini CLI, can use the CLI without any setup beyond logging in. The design choices above are what make that work well:

  • Built-in help is enough to learn the CLI. An agent can start with mixpost --help and mixpost post --help, and every command's help includes examples.
  • JSON by default. An agent's commands don't run on a terminal, so it gets JSON without asking.
  • Errors as data. Errors are JSON on stderr, with the exact field that failed and why, so the agent can fix the command and try again.
  • Natural names. The agent can write --account linkedin or -w acme instead of looking up ids first.
  • Safe by default. Deletes need an explicit --yes, and --dry-run shows what would happen first.

A typical instruction you can give an agent:

Using the mixpost CLI, write three posts about our new media folders feature for LinkedIn and X, and schedule them for the best times next week. Show me the drafts before you schedule them.

tip

To keep an agent from changing anything, give it a key that belongs to a user with the Viewer role. It can still read posts and analytics.

If your agent runs in a chat app rather than a shell, such as Claude or ChatGPT on the web, use the MCP server instead.