Documentation
falllow turns machines you already have into one compute pool. A control plane takes jobs and decides where they run; runners do the work and report back.
Quickstart
You need a control plane and at least one enrolled machine, Install covers both. With those in place, submitting work is one command from any working directory.
# on your machine, once
falllow login --server https://app.falllow.com --token flw_a_…
# from any project
falllow run -- pytest -qThe pieces
- Control plane, one process plus Postgres. It holds the queue, decides placement, keeps logs and artifacts, and serves both the API and the manager.
- Runner (
falllow-runnerd), one per machine. It dials out to the control plane and keeps the connection open, so nothing has to listen on the machine and a laptop behind CGNAT joins the same way a server does. - CLI (
falllow), how work is submitted and followed. Built to be driven by scripts and agents as much as by hand: every command takes--json. - Manager, the web app the control plane serves. Devices, jobs, logs, tokens and the GitHub bridge.
What a job is
A job is a command, a working directory and a resource reservation. The directory is packed and shipped, the command runs inside a fresh copy of it on one machine, and its output streams back while it happens. A job never spans machines: it is limited by the device it lands on.
Reservations are threads and memory, not slots. A job asking for four threads and 8 GB takes exactly that much of a machine's budget, and the machine keeps accepting work until its budget is spent.
States
| State | Meaning |
|---|---|
queued | Accepted, waiting for a machine with room. |
assigned | Leased to a runner, not started yet. |
preparing | Workspace and environment being set up. |
running | The command is executing. |
succeeded | Exited zero. |
failed | Exited non-zero, timed out, or the machinery failed. |
cancelled | Stopped on request. The process tree is killed. |
Failure means two different things
This is the distinction falllow is built around. A test suite that failed and a runner that vanished are both "the job did not succeed", and treating them the same is how you either retry a real failure into a green build or give up on a machine that only lost its network for a moment.
| Class | Cause | Retried | Exit code |
|---|---|---|---|
job_failure | The command exited non-zero. Its verdict. | Never | The command's own |
infra_failure | A runner died, a lease expired, a transfer broke. | Yes, with backoff | 170 |
timeout | Past --timeout. The tree is killed. | No | 170 |
cancelled | Someone stopped it. | No | 170 |
So falllow run -- pytest substitutes for pytest in a script:
the exit code you get is the one you would have got locally. 170 is reserved and means only one thing, the pool failed you, the command did not.
Where work goes
The scheduler prefers a machine that already has the environment warm, but only while waiting for it is cheaper than setting up cold somewhere else. It learns what a cold setup actually costs per environment and machine from the runs that had to do one, so the trade-off is measured rather than assumed.
Environments are identified by an env_key, a hash of your lockfiles,
toolchain pins, OS and architecture, computed before the job is submitted. Two jobs
with the same key need the same environment, which is what makes "warm" mean anything.
Keep reading
- Install, control plane, then machines.
- CLI, every command, the environment a job sees, and the JSON output.
- GitHub Actions, self-hosted runners on demand.