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

CommandWhat it does
loginStore an API token for this machine, verifying it immediately.
runSubmit a job and stream it. Everything after -- is the command.
psList jobs. --mine narrows it to the ones this token submitted.
logsReplay a job's output, or -f to follow it.
waitBlock until a job settles, then exit with its outcome.
showOne job in detail: attempts, timings, artifacts.
cancelStop a job and kill its process tree.
fetchDownload a job's artifacts into a directory.
devicesList enrolled machines and their live load.

Running things

falllow run
# 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
FlagMeaning
--coresThreads to reserve. The job sees this as FALLLOW_CORES. Default 1.
--memoryMemory to reserve, e.g. 8G or 512M.
--osRequire linux or windows.
--imageContainer image to run in. Every job runs in one; without this it gets the pool's default.
--gpuRequire a machine with a GPU.
--labelRequire a runner carrying this label. Repeatable.
--artifactGlob of files to collect afterwards. Repeatable.
--timeoutWall clock limit in seconds. Past it the tree is killed.
--shardSplit into this many independent jobs.
--detachReturn the job id instead of streaming to completion.
--no-workspaceDo not send the working directory.
--webhookPOST the outcome here when the job settles.
--priorityHigher 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.

Eight ways at once
falllow run --shard 8 -- pytest -q
What the command does with it
import 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

VariableValue
FALLLOW_CORESThreads reserved. Pass it on: pytest -n $FALLLOW_CORES.
FALLLOW_SHARD_INDEXThis shard's index, zero based. Only when sharding.
FALLLOW_SHARD_COUNTHow many shards there are.
PYTHONUNBUFFEREDSet 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_DIRPointed 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
$ 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.

Branching on the outcome
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" ;;
esac

Just 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
$ 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 test

This 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 --webhook
falllow run --detach --webhook https://you.example.com/hooks/falllow -- pytest

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

Collecting output
falllow run --artifact 'report/**' --artifact junit.xml -- pytest
falllow fetch <job> --into ./results

Exit codes

CodeMeaning
0The command succeeded.
anything elseThe command's own exit code, passed through unchanged.
170Reserved: 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.