Skip to the content.

Home · Agents · Reference · Design · Security

Reference

Commands

qex submit [--cpu N] [--mem SIZE] [--timeout TIME] [--needs ID,ID]
           [--after ID,ID] [--name NAME] [--job FILE] -- COMMAND...
qex wait   <id>... [--timeout TIME] [--passthrough]
qex list   [--state STATE] [--tag TAG] [--json]
qex status <id> [--json] [--show-env]
qex logs   <id> [--follow] [--tail N] [--stdout|--stderr]
qex kill   <id>...          stop a job that operates
qex cancel <id>...          remove a job from the queue
qex clean  [<id>|completed|done|--state STATE|--older-than 7d|--all]
qex info                    the coordinator: its pid, its budget and its load
qex config show             the values that qex uses now
qex schema job|status       the JSON Schema of each format
qex help <topic>

qex submit writes the job id to stdout and writes nothing else, so ID=$(qex submit ...) operates correctly. A warning goes to stderr.

Every command that reads data accepts --json.

Exit codes of qex wait

Code Meaning
0 The job succeeded.
1 The job failed.
124 Your wait reached its time limit. The job continues.
125 Something stopped the job: kill, timeout or out-of-memory.
126 The job did not run, because a job that it needed failed.
127 There is no job with that id.

The code 124 has the same meaning as the code of the timeout command. A timeout on qex wait stops your wait only. It does not stop the job.

Add --passthrough to exit with the exit code of the job.

Resource claims

Give --cpu and --mem. qex uses these claims to decide how many jobs operate together.

If you do not know the size of a task, use a word in place of a number:

Word Meaning
half, guess One half of the budget. Two such jobs operate together.
full, max The full budget. The job operates alone.

qex calculates these words against the budget, and not against the free memory of the moment. The same command thus always gives the same claim.

qex learns the size of a task

qex records what each job really used and uses those numbers as the claim for the next job of the same command:

qex submit -- cargo test    # run 1: the default claim
qex submit -- cargo test    # run 2: the claim comes from run 1

qex status says where a claim came from. The record is for the command, not the name, because cargo build and cargo test need different sizes. qex uses the largest measurement it holds plus a margin, because a claim that is too small stops the job while a claim that is a little large costs only capacity. A job that did not complete is never recorded: it shows the memory it reached, not the memory it needs.

Turn it off with [learn] enabled = false.

Do not run a small test job to measure a task. Give guess and start the real task. qex measures each job, and you can read the true use later:

qex status $ID --json      # the usage field gives max_rss and cpu_secs

Read those numbers only when you run the same kind of task many times and the queue is slow. For one task, guess is sufficient.

A claim that is larger than the budget

Such a job can never meet the usual rule. qex starts it alone when no other job operates. The job can then cause swap operations, use every core, or stop with an out-of-memory error.

Each of these results is data for you. A job that waits for ever gives no data. The status field forced is true for such a job, and qex submit writes a warning at the time of the submission.

A pipeline of stages

Do not put the stages of a pipeline in one script. If stage 3 of that script fails, you get one exit code and one log file with every stage mixed together, and you must find the cause yourself.

Give each stage its own job:

BUILD=$(qex submit --name build -- make)
TEST=$(qex submit --name test --needs $BUILD -- make test)
SHIP=$(qex submit --name ship --needs $TEST -- ./deploy.sh)
qex wait $SHIP

Keep the id of each stage and give it to the next stage.

Each stage has its own log file, its own exit code and its own claim. If build fails, test and ship do not start:

ID        STATE     NAME   ...  NOTE
a1b2c3d4  failed    build  ...  the job stopped with the exit code 2
b2c3d4e5  skipped   test   ...  the job a1b2c3d4 (build) is failed, ...
c3d4e5f6  skipped   ship   ...  the job a1b2c3d4 (build) is failed, ...

There is one failure only, and it is the cause. qex logs a1b2c3d4 gives the output of that stage, and no other output.

Each skipped job names the first job that failed, and not the job before it. A read of the last stage thus gives the cause immediately, and you do not follow the chain.

Option Meaning
--needs ID,ID Wait for these jobs. Do not run if one does not succeed.
--after ID,ID Wait for these jobs, whatever their result.

Use --after for a cleanup step that must run also when the build fails.

qex wait gives 126 for a skipped job and 1 for a job that failed, so a script can separate a failure of its own stage from a failure of an earlier stage.

Each option accepts an id or a name, and the two have different rules.

An id must exist. That is the only rule, so a script can submit its last stage even when the first stage already failed; the last stage then becomes skipped with the correct cause.

A name must give a job that is in the queue or operates. A name can give a job of an earlier run — you write --needs test, you forgot to start a new test job, and the name gives yesterday’s test job, which already succeeded. Your stage would then start immediately and wait for nothing. qex refuses that.

Use an id in a script. Use a name when you type a command yourself.

A job can name only the jobs that you started before it, so a circle of dependencies is not possible.

Job files

qex submit --job train.toml
name = "train-model"
command = ["uv", "run", "train.py", "--epochs", "50"]
timeout = "4h"
tags = ["ml"]

[resources]
cpu = 3          # or "guess", or "full"
mem = "8GB"

[env]
CUDA_VISIBLE_DEVICES = "0"

A job file also accepts needs and after:

command = ["make", "test"]
name = "test"
needs = ["build"]

qex reads TOML, YAML and JSON. The file extension selects the format.

command is a list of arguments, and it is not a shell command line. qex starts no shell, so you need no quotation marks and no escape characters. To use a shell feature, name the shell: ["bash", "-lc", "a | b > c.txt"].

A field name with a spelling error gives an error. qex does not ignore it.

The environment and the directory

qex submit copies your environment and your current directory. Your job thus operates in the same way as a command that you type now.

A later source replaces an earlier source:

environment from the shell  ->  job file [env]  ->  --env K=V
directory from the shell    ->  job file cwd    ->  --cwd D
config file defaults        ->  job file        ->  command line options

Use --env-capture minimal if your shell holds secrets. That mode copies PATH, HOME, USER, LOGNAME, SHELL, LANG and TZ only. Use --no-env-capture to copy nothing.

qex writes the captured environment to spec.json with mode 0600, and the job directory has mode 0700. qex status hides the environment. Add --show-env to see it.

Configuration

The config file is ~/.config/qex.toml. Every field is optional. Run qex config show to see the values that qex uses now.

[budget]
cpu = "75%"           # cores that qex can use
mem = "75%"           # memory that qex can use

[system]
reserve_mem  = "2GB"  # memory to keep free for other programs
max_pressure = 20     # maximum PSI memory pressure (Linux only)

[queue]
oversized = "run-when-idle"   # run-when-idle, reject or queue

[defaults]
cpu = 1               # the default is 1 core
mem = "2GB"           # the default is the machine memory / the core count
timeout = "0"         # the default is no limit

With no [defaults] section, a job gets 1 core and an equal part of the machine memory. The default job size thus scales with the machine.

Update the coordinator before you use a new option

qex refuses a field that it does not know. That rule finds a name with a spelling fault, and a name with a spelling fault must not be ignored in silence.

It has a second cause. A new option belongs in the config file only after the coordinator is the new build:

qex info                # the version and the pid of the coordinator
# install the new qex
kill <pid>              # the jobs that operate continue
qex info                # the new version now
# NOW put the new option in ~/.config/qex.toml

The program on the disk is not sufficient. A coordinator operates for hours, it holds the code that started it, and it reads the config file once, when it starts. A new option that you write before that moment has no effect, and qex ignores it in silence. The coordinator stops by itself when no job operates, and kill <pid> changes it at once.

Install the new qex before you kill the coordinator. While the old qex is the program on the disk, no coordinator can start from a file that holds the new option, and the commands in the next paragraph that need a coordinator go with it.

In the other order, qex submit, qex run, qex pipeline, qex gc, qex du and qex config show stop, and qex cannot start a coordinator — so a queue whose coordinator retires stays where it is. The jobs that operate continue. These are the commands you keep, and the second group is the larger one:

Continue in every state Continue while a coordinator operates
qex wait, qex top, qex logs, qex version qex info, qex list, qex status, qex kill, qex cancel, qex clean, qex rerun

qex rerun is in the second group, so you can still start work even though qex submit stops: it asks the coordinator for a job that the records already hold, and it needs no config file. With no coordinator, each command in the second group waits 10 seconds and then reports that the coordinator did not start. That message names no cause, which is the second reason to install the new qex first.

A job that starts in this state uses the default values, and qex status says so. Remove the section from the file to go back.

Two people or two agents that share a machine each run their own coordinator, so each must make this change for itself.

Run qex help config for every field.

More help inside the tool

Each topic below is also in the binary, so an agent needs no network:

qex help agents      the one page for an agent
qex help job-file    the fields of a job file
qex help resources   claims, the budget and the several-user accounting
qex help states      each job state and what causes it
qex help exit-codes  the exit code of each command
qex help config      each configuration field
qex schema job       the JSON Schema of a job file
qex schema status    the JSON Schema of status.json