CLI
falllow is how work is submitted and followed. Every command takes --json and --server, both of which also read from the
environment (FALLLOW_SERVER, FALLLOW_TOKEN).
Commands
| Command | What it does |
|---|---|
login | Store an API token for this machine, verifying it immediately. |
run | Submit a job and stream it. Everything after -- is the command. |
ps | List jobs. --mine narrows it to the ones this token submitted. |
logs | Replay a job's output, or -f to follow it. |
wait | Block until a job settles, then exit with its outcome. |
show | One job in detail: attempts, timings, artifacts. |
cancel | Stop a job and kill its process tree. |
fetch | Download a job's artifacts into a directory. |
devices | List enrolled machines and their live load. |
Running things
# stream it to completion, exit with its code
falllow run -- pytest -q
# reserve more of a machine
falllow run --cores 8 --memory 16G -- cargo test
# require a capability
falllow run --gpu --label cuda-12 -- python train.py
# no sources needed
falllow run --no-workspace -- nvidia-smi
# hand back the id and carry on
falllow run --detach -- ./long-thing.sh| Flag | Meaning |
|---|---|
--cores | Threads to reserve. The job sees this as FALLLOW_CORES. Default 1. |
--memory | Memory to reserve, e.g. 8G or 512M. |
--os | Require linux or windows. |
--image | Container image to run in. Every job runs in one; without this it gets the pool's default. |
--gpu | Require a machine with a GPU. |
--label | Require a runner carrying this label. Repeatable. |
--artifact | Glob of files to collect afterwards. Repeatable. |
--timeout | Wall clock limit in seconds. Past it the tree is killed. |
--shard | Split into this many independent jobs. |
--detach | Return the job id instead of streaming to completion. |
--no-workspace | Do not send the working directory. |
--webhook | POST the outcome here when the job settles. |
--priority | Higher runs first. Default 0. |
Sharding
--shard submits several independent jobs rather than splitting the work
itself, only your command knows how to divide it. Each shard is told its slice and
decides what that means.
falllow run --shard 8 -- pytest -qimport os
index = int(os.environ.get("FALLLOW_SHARD_INDEX", 0))
count = int(os.environ.get("FALLLOW_SHARD_COUNT", 1))
mine = [t for i, t in enumerate(tests) if i % count == index]Most test runners already understand this shape natively. Shards are real jobs with their own leases, retries and logs, so one shard whose machine dies is requeued without touching the others. The run's exit code is the first genuine failure among them, infrastructure failures only surface when nothing actually failed, because reporting one over a real test failure would hide the thing worth looking at.
The environment a job sees
| Variable | Value |
|---|---|
FALLLOW_CORES | Threads reserved. Pass it on: pytest -n $FALLLOW_CORES. |
FALLLOW_SHARD_INDEX | This shard's index, zero based. Only when sharding. |
FALLLOW_SHARD_COUNT | How many shards there are. |
PYTHONUNBUFFERED | Set to 1, so Python output streams instead of appearing at the end. |
UV_CACHE_DIR, PIP_CACHE_DIR, CARGO_HOME, PNPM_STORE_DIR, npm_config_cache, SCCACHE_DIR, CCACHE_DIR | Pointed at the runner's shared caches, so a warm machine links instead of downloading. |
Anything you set yourself is applied last and wins, including over these.
For scripts and agents
--json makes every command machine-readable. The shape wait prints is the same one the webhook delivers, so a caller parses the
same fields whichever it uses.
$ falllow wait --json 019fec9f-6749-7583-92e9-246899c5c491
{
"job_id": "019fec9f-6749-7583-92e9-246899c5c491",
"state": "failed",
"exit_code": 1,
"failure": "job_failure",
"message": null,
"retryable": false,
"attempt": 1,
"setup_ms": 412,
"exec_ms": 6210,
"cache_hit": true,
"device_name": "ryzen-9900x"
}retryable is the field to branch on: it is true exactly when the failure
was in the machinery rather than the work, which is the only case where running the
same thing again could produce a different answer.
falllow run --json --cores 4 -- pytest -q
case $? in
0) echo "green" ;;
170) echo "the pool failed, not the tests, safe to run again" ;;
*) echo "the tests failed" ;;
esacJust mine
The control plane's only notion of identity is the token, so --mine means
"submitted with this credential". Two tokens held by the same person are two
submitters, which is the honest answer until there are accounts.
$ falllow ps --mine
ID STATE DEVICE COMMAND
019fec9f-6749-7583-92e9-246899c5c491 running ryzen-9900x pytest -q
019fec9e-0b12-7d40-8a31-4c7f2a9e15b8 succeeded nuc-01 cargo testThis is what the desktop app shows in its jobs view. The app runs the same command rather than talking to the control plane itself, so there is one implementation of the credential, the request and the failure messages.
Getting the outcome without polling
falllow run --detach --webhook https://you.example.com/hooks/falllow -- pytestThe control plane POSTs the result once the job settles. Same body as wait --json.
Artifacts
Name what to keep when submitting; collect it afterwards. Globs are relative to the workspace.
falllow run --artifact 'report/**' --artifact junit.xml -- pytest
falllow fetch <job> --into ./resultsExit codes
| Code | Meaning |
|---|---|
0 | The command succeeded. |
| anything else | The command's own exit code, passed through unchanged. |
170 | Reserved: falllow failed, not your command. A runner died, a lease expired, the job timed out or was cancelled. |
Which is what lets falllow run -- pytest stand in for pytest in an existing script: in the pool or on your laptop, the code your script reads means
the same thing.