Scripting for agents
Lium is agent-first compute: the renter path was built to run with nobody at a keyboard. This page is the contract for a script or an agent that has only a shell — what to pass so nothing prompts, what comes back as JSON, which exit code means what, and how to rent so that a retry does not rent twice. Every command and output below was run on the released CLI on a Linux box with stdin closed.
Read AI Agents first if you have no account yet; this page starts from a key.
1. The key lives in the environment
export LIUM_API_KEY=sk_... # created once by a human at lium.io → Access → API Keys,
# or minted by POST /api/auth/signup (see AI Agents)
lium balance --json # {"balance": 12.34, "balance_usd": 12.34, "currency": "USD"} — the first call that proves the key
LIUM_API_KEYis read by everyliumcommand and by the SDK'sLium(). Nothing else is needed: the SSH key is generated on first use (~/.ssh/id_ed25519, no passphrase) when none exists.- Do not run plain
lium initfrom a script. When stdin is not a terminal it takes the--no-browserpath: it prints an approval URL and a session ID a human must act on, and exits 0 — the key is only saved oncelium init --session <ID>runs after the approval. WithLIUM_API_KEYexported,lium initis not needed at all. lium lsanswers with any key, so it proves nothing about the key (with none it exits 2,No API key found). Uselium balance --jsonorGET /users/me.- With no key,
lium balance --jsonexits 2 with{"error": {"code": "no_api_key", …}, "ok": false}on stderr; with a wrong key it exits 3 with"code": "invalid_api_key"and a message that names the key and where it was read from. Theerrorobject also carrieshintandexit_code.
2. Nothing may prompt
| Command | Pass | Why |
|---|---|---|
lium up … | --yes --no-ssh | Without --yes the rent asks Acquire pod on …? [y/n]; with stdin closed that is exit 2 with the code confirmation_required, nothing rented. --no-ssh returns when the pod is ready instead of opening an interactive SSH session. |
lium up … | --ttl 15m (or --until) | The platform removes the pod on time even when your script dies. Set it on every rent — and know that it is scheduled once the pod is ready: a rent that hits --timeout while still starting has no TTL (see §6). |
lium rm <huid> | nothing | A pod named by huid, id or name is removed without a question. --yes is only needed with --all or a row number. |
lium rm --all | --yes | Removes every pod on the key — including pods another process created with the same key. |
lium exec <pod> "cmd" | nothing | Runs over SSH, no TTY needed; exits with the remote command's exit code. |
lium ssh <pod> | avoid | Interactive. Use lium exec, or the ssh_command from lium ps --format json with -o BatchMode=yes. |
Pass the node's id to lium up, not the huid you see in the table: lium up golden-lion-86
answers Node 'golden-lion-86' not found.
3. JSON where it exists, today
| Command | Machine-readable form | Notes |
|---|---|---|
lium ls | --format json | Array of nodes: id, huid, gpu_type, gpu_count, price_per_hour, country, vram_gb, docker_in_docker, is_pareto, … |
lium ps | --format json | Array of pods: id, huid, name, status, price_per_hour, spent_usd, uptime, ip, ports, ssh_cmd, ssh_command, removal_scheduled_at. |
lium describe <pod> | --json | Objects pod, gpu, machine, ports, access (ssh_cmd, ssh_command, jupyter_url), template, storage, billing. |
lium exec <pod> "cmd" | --json | {"ok": bool, "results": [{"pod", "exit_code", "stdout", "stderr", "error"}]}; the process exit code is the remote command's. |
lium balance | --json | {"balance": 12.34, "balance_usd": 12.34, "currency": "USD"} |
lium signup, lium topup …, lium audit | --json | See their reference pages. |
lium templates | --format json | Array of templates with their ids. |
lium up, lium rm, lium logs | none | lium up prints Pod <huid> (name: <name>, id: <id>) ready; read the pod back with lium ps --format json. |
When a command with --json fails before it can do its work, stdout stays empty and stderr carries
{"ok": false, "error": {"code": "...", "message": "...", "hint": "...", "exit_code": N}}. --json is accepted
wherever --format json is, and both put the same envelope on stderr. lium exec --json is different: a remote command
that exits non-zero is a result, printed to stdout with "ok": false and the exit code inside results.
The REST API answers every error with X-Request-Id and
{"error": {"code", "message", "hint", "request_id"}, …} — see API Authentication.
4. Exit codes to branch on
0 success · 1 general error · 2 configuration (bad arguments, no key, a prompt nobody answered:
confirmation_required) · 3 the API refused or
failed the call (wrong key) · 4 SSH · 5 pod not found · 6 permission denied (a rent the balance does not
cover is a 403 → exit 6). lium exec is the exception: it exits with the remote command's code. Full table:
CLI reference → Exit codes.
5. Rent so that a retry does not rent twice
The CLI and SDK guard the rent itself (the SDK sends an Idempotency-Key, and after a lost answer looks for a
pod with the requested name before sending again) — but nothing guards a second run of your script. Two
things make a retried script safe:
- Name the pod with a name only this job uses, and look for it before renting.
- Set
--ttlso an orphan removes itself once it is ready (a pod that never got ready has no TTL — §6).
set -euo pipefail
NAME="job-$RUN_ID" # unique per job, stable across retries
POD=$(lium ps --format json | jq -r --arg n "$NAME" '.[] | select(.name==$n) | .huid' | head -1)
if [ -z "$POD" ]; then
NODE=$(lium ls --gpu RTX4090 --format json \
| jq -r '[.[] | select(.gpu_count==1)] | sort_by(.price_per_hour) | .[0].id')
lium up "$NODE" --yes --no-ssh --ttl 2h --name "$NAME" # returns when RUNNING with SSH up
POD=$(lium ps --format json | jq -r --arg n "$NAME" '.[] | select(.name==$n) | .huid')
fi
A second lium up on the same node after the first succeeded says Node '<id>' not found — the node left
the listing when it was rented. That is not a duplicate guard; the name check above is.
Never select "my pod" by position (.[0]) on a key that other jobs share: the first row is whatever pod is
oldest, not yours.
6. Waiting, ETA, timeouts
lium up waits for the pod itself and prints a line whenever the status or phase changes, and every 30 s otherwise:
pod job-42 (id: d4d510c6-…) created; waiting for it to become ready
waiting for golden-comet-17… PENDING (8 s) · est. ready in ~20 s (phase: creating volume)
waiting for golden-comet-17… PENDING (16 s) · est. ready in ~12 s (phase: configuring ssh)
waiting for golden-comet-17… RUNNING (19 s)
Pod golden-comet-17 (name: job-42, id: d4d510c6-…) ready
--timeout SECONDS(default 900) bounds the whole rent;--ready-timeout SECONDSbounds only the wait. When the budget runs out the command exits 1 (pod_not_ready), names the pod and saysAuto-termination (--ttl/--until) was NOT scheduled; it is set once the pod is ready.The pod keeps billing with no TTL: your script mustlium rmit (or wait for it withlium ps).- A pod that fails to start exits 3 (
pod_start_failed) with the huid, the last status and the status history. - In the SDK the same wait is
lium.wait_ready(pod, timeout=600); it raisesPodStartErroron a terminal status and returnsNoneonly when the timeout passes while the pod is still starting. - Polling yourself:
lium ps --format jsonevery 2 s for the first 90 s, then every 10 s, is what the CLI does.
7. Run, copy, observe
lium exec "$POD" "nvidia-smi --query-gpu=name,memory.total --format=csv,noheader"
lium scp "$POD" ./payload.txt # lands in /root/ on the pod
lium exec "$POD" --json "python train.py; exit \$?" | jq '.results[0].exit_code'
lium logs "$POD" --tail 50 # the pod's container log
lium ps --format json | jq -r --arg h "$POD" '.[] | select(.huid==$h) | .spent_usd'
Every exec connects over SSH with the key lium configured; the pod's host key is pinned on the first
connection — stderr carries Pinning ssh-ed25519 host key SHA256:… for [<ip>]:<port> (first connection to this pod; LIUM_SSH_INSECURE=1 disables pinning).
8. Tear down, and prove it
lium rm "$POD" # → Removed 1 pod(s): golden-comet-17 exit 0
lium rm "$POD" # → No pods match targets: golden-comet-17 exit 5 — already gone
lium ps --format json | jq -e --arg h "$POD" 'map(select(.huid==$h)) | length == 0' >/dev/null
Billing stops at removal. spent_usd in lium ps --format json is the CLI's estimate — uptime × the
node's hourly price — while the pod exists; the account's balance is the source of truth
(lium balance --json). Schedule a removal instead of waiting: lium rm "$POD" --in 30m.
9. The whole loop, headless
#!/usr/bin/env bash
set -euo pipefail # LIUM_API_KEY exported by the caller
NAME="job-$RUN_ID"
trap 'lium rm "$NAME" || true' EXIT # before the rent, keyed on the name: a rent that times out
# (pod_not_ready, exit 1, no TTL yet) still gets removed
POD=$(lium ps --format json | jq -r --arg n "$NAME" '.[] | select(.name==$n) | .huid' | head -1) # §5: a retry finds its pod
if [ -z "$POD" ]; then
NODE=$(lium ls --gpu RTX4090 --format json | jq -r '[.[] | select(.gpu_count==1)] | sort_by(.price_per_hour) | .[0].id')
lium up "$NODE" --yes --no-ssh --ttl 1h --name "$NAME" --timeout 600
POD=$(lium ps --format json | jq -r --arg n "$NAME" '.[] | select(.name==$n) | .huid')
fi
lium scp "$POD" ./job.py
lium exec "$POD" "cd /root && python job.py > out.txt 2>&1"
lium exec "$POD" "cat /root/out.txt"
Measured on an RTX 4090 at $0.30/h: rent to ready 22 s, exec 2.2 s, rm 1.1 s, total spend under $0.01.
Related
- AI Agents — from no account to a key, fingerprint signup, self-funding
- API Authentication — key sources and precedence, the error envelope
- CLI reference — every flag; exit codes
lium up·lium rm·lium exec- SDK —
Lium(),wait_ready,PodStartError