CLI¶
The HollowHost CLI lets you manage your AI Jobs, trigger runs, view logs, and configure secrets from your terminal.
Installation¶
macOS (recommended)¶
Linux ¶
Beta
Linux builds are provided as-is and have not been extensively tested. Please report any issues.
Verify the installation:
Upgrade¶
The CLI checks for new releases at most once a day and prints a notice on stderr when your binary is outdated. To upgrade, re-run the installation command for your platform — it always fetches the latest release.
Which version you need¶
hollowhost version prints your version. The swarms commands, swarms flows
included, need 1.23.0 or later; an older CLI answers unknown command "swarms". Creating and
editing a flow (swarms flows create, export, update, rename, schedule, delete and
validate) needs 1.24.0 or later.
Authentication¶
Log in with your HollowHost account. This opens your browser for secure authentication via OAuth 2.0.
On headless environments (SSH, CI), use --no-browser to get a URL you can open manually:
To log out and remove stored credentials:
Commands¶
AI Jobs¶
agents is still accepted
The command group is hollowhost ai-jobs. The legacy hollowhost agents
is kept as an alias during the transition, so existing scripts keep working.
List AI Jobs¶
All commands support --output json for scripting (see JSON output):
Get AI Job details¶
Create an AI Job¶
| Flag | Required | Description |
|---|---|---|
--repo |
Yes | GitHub repository (owner/repo) |
--lang |
Yes | python or typescript |
--pm |
Yes | Package manager: pip, uv (Python) or npm (TypeScript) |
--token-type |
No | CUSTOM (default), DEFAULT, or NONE; see Repository access |
--github-token |
If CUSTOM | GitHub personal access token |
--entry-point |
No | Entry point file (e.g. src/main.py) |
--branch |
No | Git branch to deploy (default: repo default branch) |
--sub-path |
No | Sub-directory to deploy for mono-repos (default: repo root) |
--project |
No | Project ID to assign the AI Job to |
--swarm |
No | Swarm ID to add the AI Job to (independent from --project) |
--follow |
No | Watch validation progress |
When --sub-path is set, the AI Job is built from that sub-directory and
--entry-point stays relative to the sub-path (write src/main.py, not
apps/agent-meteo/src/main.py).
# Deploy the apps/agent-meteo sub-directory from the dev branch of a mono-repo
hollowhost ai-jobs create \
--repo owner/mono-repo \
--lang python \
--pm uv \
--branch dev \
--sub-path apps/agent-meteo \
--entry-point src/main.py
No name at creation. create takes no --name: a new AI Job shows as its repository's name
until you give it one with hollowhost ai-jobs update <ai-job-id> --name "..." (or on the AI Job's
Settings tab in the web app).
Set for good at creation: the repository, branch, sub-path, language and package manager. The
CLI, the API and the web app can't change them afterwards; to deploy another branch or sub-path,
create another AI Job (duplicate copies them as they are).
Repository access¶
--token-type says how HollowHost reads your repository: to check it when the AI Job is created,
and to build it at each deploy.
| Value | For | Token used |
|---|---|---|
NONE |
A public repository | None |
DEFAULT |
Private repositories, with one token for all your AI Jobs | The GitHub token saved on your account |
CUSTOM (the CLI's default) |
A private repository with a token of its own | --github-token, kept for this AI Job only |
The account's token is set in the web app, under Settings → GitHub Token; the CLI can't set
it. Use a fine-grained token with Contents: Read-only on your repositories, or a classic token
with the repo scope (ghp_… or github_pat_…). It's read at each check and each deploy, so a
new token applies to every DEFAULT AI Job at its next deploy. Without one, a DEFAULT AI Job
ends in VALIDATION_ERROR: No default GitHub token configured in your profile.
Deploy an AI Job¶
hollowhost ai-jobs deploy <ai-job-id>
# Watch the deployment in real time
hollowhost ai-jobs deploy <ai-job-id> --follow
Deploy an AI Job once it's VALIDATED. Deploy it again to ship new commits or settings (from
DEPLOYED), or to retry after an error. Don't deploy while it's VALIDATING or DEPLOYING: the
deploy isn't refused, and it starts a second build.
While it deploys, the AI Job is DEPLOYING and refuses runs, scheduled ones included. A failed
deploy leaves DEPLOYMENT_ERROR, which refuses runs until a deploy succeeds.
Right after the first deploy, an AI Job can show DEPLOYED a few seconds before it can run. A
run started then waits up to 20 seconds; if the AI Job still isn't ready, the run is refused with
Agent Lambda is still initializing (503): start it again a little later.
Run an AI Job¶
hollowhost ai-jobs run <ai-job-id>
# Watch execution and display logs when done
hollowhost ai-jobs run <ai-job-id> --follow
# Pass an event payload — exposed to your code as the HH_EVENT env var (JSON string)
hollowhost ai-jobs run <ai-job-id> --event '{"mode": "backfill", "day": "2026-07-14"}'
hollowhost ai-jobs run <ai-job-id> --event-file event.json
At runtime your code also receives HH_RUN_ID (the run's identifier),
HH_SOURCE (api, scheduler, or flow for a step of a flow), and — for
scheduled runs — HH_SCHEDULED_TIME (the ISO timestamp the schedule fired at).
A step of a flow gets flow even when a schedule started the flow: code that
checks for scheduler should check for flow too.
See Event payload format for the
full contract (envelope, 128 KB cap, error codes, runtime examples).
In the web app, the same payload can be sent with Run with event… on the AI Job page.
Only a DEPLOYED AI Job runs, one run at a time. A start while a run is going is refused: it
is never queued, and never runs alongside.
ai-jobs run(like Run in the web app, or the API) gets409 RUN_ALREADY_IN_PROGRESS.- A start from the AI Job's schedule is skipped.
- A step of a flow fails (see Swarm Flows).
Each refused start shows in the AI Job's runs as canceled, with its reason.
Update an AI Job¶
| Flag | Description |
|---|---|
--name |
Display name (max 50 chars) |
--description |
Description (max 200 chars) |
--memory-size |
Memory: 256, 512, or 1024 MB |
--timeout |
Timeout: 60-900 seconds |
--entry-point |
Entry point file path |
--persistent-storage / --no-persistent-storage |
Enable / disable persistent storage |
--token-type |
CUSTOM, DEFAULT, or NONE |
--github-token |
GitHub token (when --token-type CUSTOM) |
--project |
Project ID (empty string to unassign) |
--swarm |
Swarm ID (empty string to make it standalone); see Swarms |
--yes |
With --swarm, move it from another swarm without asking |
--schedule |
Cron expression, 5 fields (e.g. "0 9 * * MON-FRI"); see Schedule an AI Job |
--schedule-timezone |
Timezone (e.g. Europe/Paris) |
--enable-schedule / --disable-schedule |
Switch the schedule on or off, keeping its cron and time zone |
--remove-schedule |
Remove the schedule entirely |
When a change applies¶
| Change | Applies |
|---|---|
| Environment variables, secrets | At the next run |
| Schedule, name, description, project, swarm | Right away |
| Memory, timeout, persistent storage, entry point | At the next deploy |
--token-type |
The repository is checked again: see below |
update prints ✓ AI Job updated. either way: after a change that needs it, run
hollowhost ai-jobs deploy <ai-job-id>.
Changing --token-type, even to the same value, checks the repository again: the AI Job goes
back to VALIDATING, then VALIDATED (or VALIDATION_ERROR), and is no longer DEPLOYED. Its runs and its schedule are
refused until you deploy it again.
Schedule an AI Job¶
hollowhost ai-jobs update <ai-job-id> --schedule "0 8 * * MON-FRI" --schedule-timezone Europe/Paris --enable-schedule
hollowhost ai-jobs update <ai-job-id> --disable-schedule # keeps the cron, to switch it back on later
hollowhost ai-jobs update <ai-job-id> --remove-schedule
The schedule is a 5-field cron, minute hour day-of-month month day-of-week, read in its time zone:
- Days of the week are
0to6from Sunday (7is Sunday too), or their names in any case:SUN,MON-FRI,sat,sun. - Set the day of the month or the day of the week, not both, and no step (
/) in the day of the week. A schedule that can't run is refused withINVALID_SCHEDULE. - Each flag changes only what it names.
--enable-schedulealone switches the schedule on without touching its cron or time zone;--schedulealone keeps the time zone and on/off state. - A first schedule is in UTC and switched off, unless you also pass
--schedule-timezoneand--enable-schedule. - A schedule starts runs only while the AI Job is
DEPLOYED.
Before 1.23.0 (October 28, 2026), a first schedule saved switched off was dropped without an
error, and --enable-schedule, --disable-schedule or --schedule-timezone used alone removed the
schedule. The fix is on HollowHost's side, so it applies whatever your CLI version.
Delete an AI Job¶
It deletes the AI Job and what goes with it: its builds, logs, secrets and stored files.
hollowhost ai-jobs delete <ai-job-id>
# Skip confirmation
hollowhost ai-jobs delete <ai-job-id> --yes
Duplicate an AI Job¶
Environment variables¶
Import variables from a .env file. An interactive picker lets you classify each variable as an environment variable or a secret. Sensitive names (containing KEY, SECRET, TOKEN, PASSWORD, etc.) are pre-classified as secrets.
Non-interactive mode for CI/scripts:
# Specify which variables are secrets
hollowhost ai-jobs env import <ai-job-id> --file .env --secrets "API_KEY,DB_PASSWORD"
# All as env vars
hollowhost ai-jobs env import <ai-job-id> --file .env --all-env
# All as secrets
hollowhost ai-jobs env import <ai-job-id> --file .env --all-secrets
Each list an import sends replaces the stored one. The file's variables and its secrets go as two lists:
- a variable or secret that isn't in the file is removed from its list;
- a list the import doesn't send is kept: with
--all-env, or when you mark nothing as a secret, your secrets stay as they are (and your variables with--all-secrets); - so an import can't empty a list.
Empty values are imported empty: FOO= gives FOO an empty value, it isn't skipped.
A name can't be both a variable and a secret: the import is refused with 400, Secret names
conflict with environment variable names: …. An import of secrets only is compared with the
variables already stored. To turn a variable into a secret, import a file that has all your
variables, with that one marked as a secret.
Variables and secrets apply at the next run, with no deploy.
Note
Values are never displayed in the terminal. Only variable names are shown.
Runs¶
# List runs for an AI Job
hollowhost runs list <ai-job-id>
# View logs for a specific run
hollowhost runs logs <ai-job-id> <run-id>
Projects¶
hollowhost projects list
hollowhost projects create --name "My Project"
hollowhost projects rename <project-id> --name "New Name"
hollowhost projects delete <project-id> --yes
A project files AI Jobs (ai-jobs create --project, ai-jobs update --project) and swarms
(swarms create --project, swarms update --project) to sort them in the web app; ai-jobs list
shows each AI Job's project.
- Names don't have to be unique (1 to 50 characters): two projects can have the same name, so
scripts should use the project's id, which
projects listshows. - Deleting a project is refused while AI Jobs are in it (
409): take them out first withhollowhost ai-jobs update <ai-job-id> --project "". Swarms never block it: they are left with no project.
Swarms¶
A swarm groups AI Jobs that work together (for example a chain of jobs that prepare, write and publish content) so you can follow them as one unit. It never changes the AI Jobs themselves: their project, schedule, secrets and runs stay as they are, and an AI Job belongs to at most one swarm.
hollowhost swarms create --name seo-swarm --description "SEO growth swarm"
hollowhost swarms create --name seo-swarm --project <project-id> # files the swarm in a project
hollowhost swarms list # name, AI Jobs, id, health
hollowhost swarms get <swarm-id> # members with their schedule and last run
hollowhost swarms add-job <swarm-id> <ai-job-id> [<ai-job-id>...]
hollowhost swarms remove-job <swarm-id> <ai-job-id>
hollowhost swarms runs <swarm-id> --limit 50
hollowhost swarms update <swarm-id> --name new-name
hollowhost swarms update <swarm-id> --project "" # takes it out of its project
hollowhost swarms delete <swarm-id> --yes # the AI Jobs are kept and become standalone
A swarm's project files the swarm itself: the web app's swarm list can be filtered by it. It
never applies to the swarm's AI Jobs, which keep their own project; swarms get shows both.
Names: a swarm's name is unique among your swarms, ignoring case, so SEO and seo can't both
exist (409 SWARM_NAME_TAKEN). A flow's name is unique within its swarm (409 FLOW_NAME_TAKEN).
AI Job and project names don't have to be unique: scripts should use ids.
Health is OK when the last run of every member is SUCCESS. A member
whose last run failed or was canceled makes the swarm DEGRADED, or DOWN
when no member succeeded. Members that never ran are ignored; a swarm with no
run yet is PENDING.
Moving an AI Job that already belongs to another swarm asks for
confirmation, and nothing is changed if you answer no. Pass --yes in scripts:
hollowhost swarms add-job <swarm-id> <ai-job-id> --yes
hollowhost ai-jobs update <ai-job-id> --swarm <swarm-id> --yes
hollowhost ai-jobs update <ai-job-id> --swarm "" # make it standalone again
hollowhost ai-jobs list shows a SWARM column and accepts
--swarm <id-or-name> to list one swarm's AI Jobs; hollowhost ai-jobs get
shows the swarm, and -o json includes swarmId and swarmName.
Flows¶
A flow runs some of a swarm's AI Jobs together, in the order its links give. Draw it in the web app's flow editor, or write it as JSON and save it with the CLI (see Create and edit a flow).
hollowhost swarms flows list <swarm-id> # name, steps, schedule, id, last run
hollowhost swarms flows get <flow-id> # each step in order: when it starts, what follows
hollowhost swarms flows run <flow-id> # like Run flow; prints the flow run's id
hollowhost swarms flows run <flow-id> --event '{"day": "2026-07-14"}' # the first steps get it as HH_EVENT
hollowhost swarms flows runs <flow-id> # newest first, with the steps counted by state
hollowhost swarms flows runs <flow-id> --steps --limit 5 # each step: state, duration, AI Job run
flows run takes --event or --event-file, like ai-jobs run. It is refused while the
flow runs (FLOW_ALREADY_RUNNING) or past its start limit (FLOW_RATE_LIMITED).
A step's result, the JSON it wrote to HH_RESULT_PATH for the next step (see
Pass a result to the next step of a flow),
shows in the web app, in the step's panel on the flow page; the CLI doesn't show it.
flows runs --steps gives each step's AI Job run id, and hollowhost runs logs <ai-job-id> <run-id>
shows its log.
More runs: swarms runs and swarms flows runs show one page, newest first (50 and 20 runs
by default; --limit up to 200 and 50). When there are more, the last line prints the command
for the next page, with --next-token.
Deleting an AI Job, or taking it out of its swarm, is refused while it is a step of a flow:
the error lists those flows, each with its flows get command. Take it out of them in the web
app's flow editor, or with flows export and flows update.
Create and edit a flow¶
hollowhost swarms flows create <swarm-id> --file flow.json # prints the new flow's id
hollowhost swarms flows export <flow-id> > flow.json # the flow as JSON, with its version
hollowhost swarms flows update <flow-id> --file flow.json # saves the whole flow from the file
hollowhost swarms flows rename <flow-id> --name "Evening briefing"
hollowhost swarms flows schedule <flow-id> --cron "0 7 * * 1-5" --timezone Europe/Paris --enable
hollowhost swarms flows schedule <flow-id> --disable # the flow starts only with Run flow
hollowhost swarms flows delete <flow-id> # its AI Jobs and their runs are kept
hollowhost swarms flows validate --file flow.json # checks the file offline, saves nothing
- The file is the API's flow JSON:
name,nodes(the steps),edges(the links) andschedule. The CLI sends it as is and the API checks it: a refusal prints the API's message and code, like[FLOW_HAS_CYCLE](see Errors).--file -reads it from stdin. - To edit a flow, export it, change the file, then
updateit.versiontells the API which save you changed: if the flow was saved since,updateis refused (409 FLOW_VERSION_CONFLICT), and you export it again and redo your change.flowId,swarmId,createdAtandupdatedAtare read only; a file withoutschedulekeeps the flow's schedule. - To copy a flow, export it, change its
name, thencreateit. renameandscheduleneed no export: they don't take the version, like the web app's Schedule switch, so a save made since doesn't refuse them.schedulechanges only what you pass (--enablekeeps the cron,--cronkeeps the time zone), and--removeremoves the schedule.- A new schedule is off unless you pass
--enable, or the file says"enabled": true. While the flow's schedule is on,create,updateandschedulename the AI Jobs that no longer run on their own schedule (the steps withturnOffOwnSchedule). deleteasks first: pass--yesin scripts.validatechecks a file with the API's rules without calling it: its fields, the shape of its schedule and its graph (1 to 20 steps, each AI Job once, no link twice, no loop). It lists every problem, in the API's words, and exits with code 1 if there is one. It can't check what only the API knows (that the AI Jobs are in the swarm, that the name is free, your plan, the cron and the time zone), andcreateandupdatedon't run it: the API checks the file they send.
# Change the second step's wait in one line
hollowhost swarms flows export <flow-id> | jq '.nodes[1].waitMinutes = 10' | hollowhost swarms flows update <flow-id> --file -
Storage¶
Persistent storage keeps files from one run of an AI Job to the next. Turn it on with
hollowhost ai-jobs update <ai-job-id> --persistent-storage, then deploy the AI Job.
- Where: your code reads and writes the folder named in the
PERSISTENT_STORAGE_PATHenvironment variable (/tmp/data). - When it's saved: before each run, the folder is filled with the saved files. After a run that succeeds, the folder is saved as it is, so the files your code deleted are deleted too. A run that fails saves nothing.
- Size: up to 1 GiB per AI Job. A run that leaves more fails with
StorageQuotaExceededand saves nothing. Today the folder sits on the AI Job's 512 MB/tmpdisk, so keep it under that. - Turning it off (
--no-persistent-storage, then deploy) keeps the files: the AI Job stops loading and saving them, andstorage fileslists nothing. Turn it back on and deploy, and they are back.storage clearerases them. - Deleting the AI Job erases its files.
- Moving to a plan without persistent storage, or cancelling your subscription, turns it off and erases the files of every AI Job whose storage is on at that moment. An AI Job whose storage was already off keeps its files.
# View storage info
hollowhost storage info <ai-job-id>
# List files
hollowhost storage files <ai-job-id>
# Delete a file
hollowhost storage delete-file <ai-job-id> path/to/file.json
# Clear all files
hollowhost storage clear <ai-job-id> --yes
Shell completion¶
Enable autocompletion for your shell:
Restart your shell or run source ~/.zshrc (or equivalent) for changes to take effect.
Global flags¶
| Flag | Description |
|---|---|
-o, --output |
Output format: text (default) or json; see JSON output |
-h, --help |
Show help for any command |
JSON output¶
With -o json, a command prints the API's answer as is, on stdout. Its fields are the API's: an AI
Job's id is id in ai-jobs get and ai-jobs list, and agentId elsewhere.
| Commands | |
|---|---|
ai-jobs list / get, runs list / logs, projects list / create / rename, swarms list / get / create / update / runs, swarms flows list / get / run / runs, storage … |
{"data": {…}}: one object (agent, project, swarm, flow, flowRun…) or a list (agents, runs, projects, swarms, flows, flowRuns, files), with nextToken when a list has more pages |
ai-jobs create, ai-jobs duplicate |
{"message": "Success!", "agentId": "…"} |
ai-jobs run |
{"data": {"runId": "…", "agent": {…}}} |
ai-jobs deploy, ai-jobs update, ai-jobs delete |
{"message": "…"}; update adds previousSwarmId when you pass --swarm |
ai-jobs env import |
{"envVarsCount", "secretsCount", "envVarNames", "secretNames"}: names, never values |
swarms add-job |
{"results": […]}: the API's answer for each AI Job, {"data": {"agentId", "swarmId", "previousSwarmId"}} |
swarms flows create / update / rename / schedule |
{"data": {"flow": {…}, "ownSchedulesTurnedOff": […], "ownSchedulesNotTurnedOff": […]}}: the flow as saved, then the AI Jobs' ids |
swarms flows export |
The flow alone, {"flowId", "name", "nodes", …}, with or without -o json |
swarms flows validate |
Text, with or without -o json: the problems go to stderr, and the exit code says if there are any |
swarms delete, swarms remove-job, swarms flows delete, projects delete |
Text: the API sends nothing back |
- Errors print on stderr, never on stdout, like
Error: API error (409): … [RUN_ALREADY_IN_PROGRESS], and the command exits with code 1. When there is a code in brackets, it's the one to test for. --follow:ai-jobs deploy --followandai-jobs run --followprint text, even with-o json;ai-jobs create -o jsonprints its answer and doesn't follow.- Confirmations (
ai-jobs delete,projects delete,swarms delete,swarms flows delete,storage clear, moving an AI Job between swarms) ask on stdout: pass--yesin scripts.