Skip to main content

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_KEY is read by every lium command and by the SDK's Lium(). 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 init from a script. When stdin is not a terminal it takes the --no-browser path: it prints an approval URL and a session ID a human must act on, and exits 0 — the key is only saved once lium init --session <ID> runs after the approval. With LIUM_API_KEY exported, lium init is not needed at all.
  • lium ls answers with any key, so it proves nothing about the key (with none it exits 2, No API key found). Use lium balance --json or GET /users/me.
  • With no key, lium balance --json exits 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. The error object also carries hint and exit_code.

2. Nothing may prompt

CommandPassWhy
lium up …--yes --no-sshWithout --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>nothingA 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--yesRemoves every pod on the key — including pods another process created with the same key.
lium exec <pod> "cmd"nothingRuns over SSH, no TTY needed; exits with the remote command's exit code.
lium ssh <pod>avoidInteractive. 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

CommandMachine-readable formNotes
lium ls--format jsonArray of nodes: id, huid, gpu_type, gpu_count, price_per_hour, country, vram_gb, docker_in_docker, is_pareto, …
lium ps--format jsonArray of pods: id, huid, name, status, price_per_hour, spent_usd, uptime, ip, ports, ssh_cmd, ssh_command, removal_scheduled_at.
lium describe <pod>--jsonObjects 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--jsonSee their reference pages.
lium templates--format jsonArray of templates with their ids.
lium up, lium rm, lium logsnonelium 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:

  1. Name the pod with a name only this job uses, and look for it before renting.
  2. Set --ttl so 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 SECONDS bounds only the wait. When the budget runs out the command exits 1 (pod_not_ready), names the pod and says Auto-termination (--ttl/--until) was NOT scheduled; it is set once the pod is ready. The pod keeps billing with no TTL: your script must lium rm it (or wait for it with lium 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 raises PodStartError on a terminal status and returns None only when the timeout passes while the pod is still starting.
  • Polling yourself: lium ps --format json every 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.