- name
- lightning-studios
- description
- Manage Lightning AI Studios (cloud dev machines with CPUs/GPUs) - create, start, stop, delete studios, switch machine types, run commands in them, upload/download files, and SSH in. Use when the user wants to work with lightning.ai Studios, needs a cloud GPU dev box, or asks to run something "on a studio".
# Lightning AI Studios
A Studio is a persistent cloud development machine on [lightning.ai](https://lightning.ai). Its filesystem persists across sessions; compute (CPU/GPU) attaches on start and detaches on stop. Everything below uses the `lightning` CLI and `lightning_sdk` Python package.
## Setup & auth
```bash
# Use the Lightning AI CLI from the current env; install or upgrade it there if it's missing or older than 2026.9.18
v=$(lightning --version 2>/dev/null | sed -n 's/^Lightning CLI version //p')
[ -n "$v" ] && [ "$(printf '%s\n' 2026.9.18 "$v" | sort -V | head -1)" = 2026.9.18 ] \
|| uv pip install -U lightning-sdk || python3 -m pip install -U lightning-sdk
lightning --version # must print "Lightning CLI version …"; if not, see the table below
lightning login # browser flow; or set env vars for headless use:
export LIGHTNING_API_KEY=... LIGHTNING_USER_ID=... # USER_ID optional: with it Basic auth, without it the key is a Bearer token
```
This uses, and if needed installs into, the agent's current environment (project venv, conda env,
…); then call plain `lightning …` everywhere. If setup doesn't go cleanly:
| Symptom | Fix |
|---|---|
| No active env, `externally-managed-environment`, or a conflict with the project's own pins | Install it on its own: `uv tool install lightning-sdk` (or `pipx install lightning-sdk`), and call `"$(uv tool dir --bin)/lightning"` if that isn't on `PATH` |
| Still no `Lightning CLI version` line after installing | Another `lightning` (PyTorch Lightning's) or an inactive env comes first: check `command -v lightning`, then activate the env or call its `bin/lightning` |
| `No such command`, `No such option` or `unexpected extra argument` | The CLI is too old: upgrade it where `command -v lightning` points (`uv pip install -U lightning-sdk`, or `uv tool upgrade lightning-sdk`) |
| Installing is blocked (read-only env or home, agent sandbox) | Run each command as `UV_CACHE_DIR="${TMPDIR:-/tmp}/uv" UV_TOOL_DIR="${TMPDIR:-/tmp}/uv-tools" uvx lightning-sdk …` |
| SSL/CA errors on every call, even `lightning --version`, only inside an agent sandbox | A `**/*.pem` read-deny rule hides certifi's `cacert.pem`: allow that one path; don't disable the sandbox |
| `NameResolutionError`, only inside an agent sandbox | If `lightning api` fails too, ask the user to allow `lightning.ai` and `*.lightning.ai` (`/sandbox`). If only `studio`/`job` commands or the SDK fail, they bypass the sandbox proxy: run just those outside it. Background pollers fail the same way, silently |
Credentials are stored in `~/.lightning/credentials.json`.
The install above also makes `lightning_sdk` importable in that env, so Python snippets run with
its `python`. If the CLI came from a uv tool or `uvx` fallback instead, run one-off scripts with
`uv run --with lightning-sdk python script.py`. For code that stays in the user's project, ask,
then declare `lightning-sdk` as a dependency with the project's own tool (`uv add`, `poetry add`).
## Resolving org and teamspace (do this first)
Every studio lives in a teamspace owned by either an organization or a user. **Never guess.** Resolve in this order:
1. Explicit `--teamspace owner/teamspace` flag (owner = org or user name) / `Teamspace("owner/teamspace")` in Python (the separate `org=`/`user=` arguments are deprecated).
2. Env vars `LIGHTNING_ORG`, `LIGHTNING_TEAMSPACE`, `LIGHTNING_USERNAME`, or config defaults in `~/.lightning/config.yaml` (`lightning config get teamspace`).
3. Otherwise list the options and **ask the user which org/teamspace to use**:
```bash
lightning api /v1/memberships | jq -r '.memberships[] | [.ownerType, .name, .projectId] | @tsv'
```
Persist the user's choice so they aren't asked again: `lightning config set teamspace <owner>/<teamspace>`.
**From a scoped API key** (an agent with no user to ask): `--teamspace` needs the owner's *slug*,
while `/v1/memberships` only gives the owner's id, and the same teamspace can appear twice (an
`organization` row and a `user` row with one `projectId`). Continue only if a single teamspace is
listed, prefer its organization row, and resolve the owner by `ownerType` (needs `jq`):
```bash
M=$(lightning api /v1/memberships)
if [ "$(printf "%s" "$M" | jq '[.memberships[].projectId] | unique | length')" != 1 ]; then
echo "several teamspaces: ask the user which one" >&2
else
ROW=$(printf "%s" "$M" | jq -c '(.memberships | map(select(.ownerType=="organization"))) + .memberships | .[0]')
TS=$(printf "%s" "$ROW" | jq -r .name); OID=$(printf "%s" "$ROW" | jq -r .ownerId)
if [ "$(printf "%s" "$ROW" | jq -r .ownerType)" = organization ]; then
OWNER=$(lightning api "/v1/orgs/$OID" | jq -r .name)
else # a personal teamspace; the search is fuzzy, so match the id exactly
OWNER=$(lightning api /v1/users/search -X GET -f "query=$OID" \
| jq -r --arg id "$OID" '.users[] | select(.id==$id) | .username')
fi
lightning config set teamspace "$OWNER/$TS" # or pass --teamspace "$OWNER/$TS" each time
fi
```
## CLI reference
`--teamspace` always takes `owner/teamspace`. Omitting `--name`/`--teamspace` in an interactive terminal opens a picker menu; in scripts always pass them explicitly.
```bash
# create (registers the studio, does NOT attach compute)
lightning studio create --name my-studio --teamspace owner/teamspace [--cloud PROVIDER] [--studio-type TEMPLATE]
# start compute (blocking; --create makes it if missing)
lightning studio start --name my-studio --teamspace owner/teamspace --machine CPU [--create] [--interruptible]
lightning studio start --name my-studio --teamspace owner/teamspace --gpus L4:4 # --gpus and --machine are mutually exclusive
# manage
lightning studio list --teamspace owner/teamspace [--all] [--sort-by status] # default lists only your studios
lightning studio switch --name my-studio --teamspace owner/teamspace --machine A100 # requires Running studio
lightning studio stop --name my-studio --teamspace owner/teamspace
lightning studio delete my-studio --teamspace owner/teamspace -y # NAME is positional here; -y/--yes: required non-interactively
# ssh / one-shot connect (create + start + ssh)
lightning studio ssh --name my-studio --teamspace owner/teamspace
lightning studio connect my-studio --teamspace owner/teamspace --machine CPU
# machine names the CLI accepts (offline list; says nothing about availability)
lightning machine list [--json]
```
### Files: `lit://` paths
Studio paths use `lit://<owner>/<teamspace>/studios/<studio-name>/<path>`. Exactly one side of a copy must be `lit://`; directories need `-r`.
```bash
lightning cp ./train.py lit://owner/teamspace/studios/my-studio/train.py # upload
lightning cp -r lit://owner/teamspace/studios/my-studio/logs/ ./logs # download dir
lightning ls lit://owner/teamspace/studios/my-studio # list files (-r recursive, --json)
lightning rm lit://owner/teamspace/studios/my-studio/old.txt [-r] [-f]
```
**`cp -r` copies a folder's contents, not the folder.** `lightning cp -r ./proj lit://…/studios/my-studio/`
puts `proj`'s files straight into the Studio's home, whether or not either path ends in `/`. To
keep the folder, name it in the destination: `lightning cp -r ./proj lit://…/studios/my-studio/proj/`.
Check the result with `lightning ls -r` before running anything that expects the files in place.
**Uploads reach the machine late, even on a ready Studio.** `lightning cp` and
`studio.upload_file` / `upload_folder` go through storage and return before the file is on the
Studio's disk, so a command launched right after can fail with `No such file or directory`. Wait
until the file is there before running anything that needs it: `upload_landed` in *Example
workflows* does this.
## Python SDK
```python
from lightning_sdk import Machine, Studio, Teamspace
ts = Teamspace("my-org/my-teamspace") # owner/teamspace; org=/user= are deprecated
studio = Studio(name="my-studio", teamspace=ts, create_ok=True) # create_ok=False -> error if missing
studio.start(machine=Machine.CPU) # blocking; interruptible=True for spot pricing
print(studio.status) # NotCreated|Pending|Running|Stopping|Stopped|Completed|Failed
# run commands — studio must be Running
out = studio.run("nvidia-smi") # raises RuntimeError on non-zero exit
out, code = studio.run_with_exit_code("pytest -q") # no raise on non-zero
out, code = studio.run_and_detach("python train.py", timeout=30) # keeps running in background
studio.upload_file("train.py", remote_path="train.py") # remote path relative to studio home
studio.download_file("outputs/model.ckpt", "model.ckpt")
studio.upload_folder("./src"); studio.download_folder("outputs/")
studio.switch_machine(Machine.A100) # change machine while running
studio.set_env({"WANDB_API_KEY": "..."}) # env vars; partial=True merges
studio.stop() # releases compute, keeps filesystem
studio.delete()
```
### Machine types
Pass as `Machine.<NAME>` or string (`Machine.from_str("A100")` accepts name or slug):
- CPU: `CPU_SMALL`, `CPU` (default, 4 cores), `CPU_X_2/4/8/16`; big-disk: `DATA_PREP`, `DATA_PREP_MAX`, `DATA_PREP_ULTRA`
- GPU: `T4_SMALL`, `T4`, `T4_X_2/4/8`, `L4`, `L4_X_2/4/8`, `L40S`, `L40S_X_2/4/8`, `RTXP_6000` (+`_X_2/4/8`), `A100` (+`_X_2/4/8`), `H100` (+`_X_2/4/8`), `H200` (+`_X_2/4/8`), `B200`, `B200_X_8`
- `A100_40GB*`/`A100_80GB*` variants exist in the SDK but are hidden from CLI `--machine` (usable with `studio switch` and in Python).
SKUs get added, so treat this list as a starting point, not a closed set. `lightning machine list`
shows the names your CLI accepts, not what a teamspace can launch.
**Pick the machine from live data, per cloud account.** A teamspace can launch on several cloud
accounts, and the same GPU differs between them in name, price and wait. List what each account
can start right now, fastest first:
```python
from lightning_sdk import Teamspace
ts = Teamspace("my-org/my-teamspace")
FAMILY = "H100" # the GPU family the workload needs
# Accounts go by id: ts.cloud_accounts holds display names, and list_machines() returns [] for a name
rows = [(m.wait_time, m.cost, a.cluster_id, m) for a in ts.cloud_account_objs
for m in ts.list_machines(cloud_account=a.cluster_id) if m.family == FAMILY]
for wait, cost, acct, m in sorted(rows, key=lambda r: (r[0] is None, r[0] or 0, r[1] or 0)):
print(f"{acct:34} {m.name:28} x{m.accelerator_count} ${cost}/h wait ~{wait}s")
```
`cost` is USD per hour and `wait_time` the expected seconds until a machine is free.
`list_machines()` with no argument merges several accounts without saying which row belongs to
which. Show the user the top rows with price and wait. **Ask before starting any GPU, and wait for a yes.** Show the machine, its account, the price
per hour and the most the run can cost under its deadline. Ask even when the user named the GPU
or gave a budget: a budget is a limit, not approval to spend it.
**A Studio's cloud account is fixed at creation (`studio switch` can't move it), so create it on
the chosen row's account** and start or switch it to that row's machine. What matters is that the
account sells that GPU at that count, not its name: the SDK maps `H200`, `lit-h200-1` and
`lit-h200-141gb-1` to one machine.
**`cloud=` / `--cloud` takes the account id (`cluster_id`), not a display name.**
`Studio(..., cloud="Lightning Cloud")` fails with `400 clusterID Lightning Cloud is invalid`.
For quotes before login, `GET /v1/core/accelerators?cloudProvider=<PROVIDER>` needs no auth (the
`lightning-cost-estimation` skill has the provider values and the costing recipes). Don't invent a
catalog endpoint: `/v1/accelerators`, `/v1/accelerator-catalog`, `/v1/pricing` and
`/v1/compute/accelerators` all return `code: 5`.
Interruptible (spot) is a flag, not a machine type: `--interruptible` / `interruptible=True`.
### Launching on a GPU
<!-- TODO: remove this workaround once the backend rejects a GPU request it can't fill instead of
starting a CPU machine (Task Board: "CLI silently downgrades to CPU when a GPU SKU isn't available"). -->
**Ask for a GPU the Studio's cloud doesn't sell and `studio start` comes up on CPU with only a
"hasn't been vetted" warning.** The default cloud doesn't sell every GPU at every count (e.g. 1×
H200 isn't on AWS). The first workflow below guards against this:
1. **Create the Studio on an account that sells the GPU at your count** (listing above). A reused
Studio keeps its old cloud: check it in `lightning studio list`, and create a new one if wrong.
2. **Do setup on CPU on that account, because installs, model downloads and CUDA source builds bill
at GPU rates otherwise.** Builds need their target set, e.g. `TORCH_CUDA_ARCH_LIST=9.0` for
H100/H200.
3. **After switching to that row's `Machine`, wait until the Studio can run commands and check the
hardware.** No GPU means stop the Studio and go back to step 1; don't retry.
## Example workflows
Prompts this skill handles: *"spin up a GPU studio and run my training script"*, *"copy this repo to my studio and start a long run"*, *"SSH into exp-studio"*, *"my studio is idle, stop it"*.
**Set up on CPU, switch to a GPU, run, collect results, stop.** `Running` comes before a Studio
can run commands, and `studio.start()` / `lightning studio start` can keep blocking after it can,
or never return. So start in the background and poll for readiness with a deadline (see Gotchas).
Other SDK calls (`run*`, `switch_machine`, `stop`) poll with no time limit of their own, so every
call below goes through `bounded`: a stalled call raises instead of billing forever, and the
`except` stops the Studio. The `lightning-jobs` skill reuses the helpers below, with their imports:
```python
import hashlib, os, re, shlex, threading, time
View on GitHub