# tama Machines with GPUs, driven from a shell. One binary, no daemon, nothing to configure. A machine is a Linux VM you can SSH into, run an agent inside, snapshot mid-task, fork and stop. You are billed for the seconds a machine is running; a stopped machine is billed for nothing, and stopping snapshots it on the way down. Read this before the first command. It covers the Python and TypeScript SDKs and the whole CLI in the order you would use them, then eight tasks worked end to end. Every flag, both SDK quickstarts, and the lines each command prints are at https://tama.computer/docs ## Rules - One card per machine. Leave `--gpu-count` at 1: every machine is created with checkpointing on, checkpointing more than one card is not supported, and a higher count fails at create rather than coming back as a machine that cannot be stopped. - Work under `/workspace`. It is the durable 50 GiB disk, every shell starts there, and it is the only place edits survive a stop. - Run `tama offers` before `tama new`. It lists the cards that exist, their rate and the size they come with; a card missing from it is a card `new` will refuse. - Keys are workspace secrets, never files in the machine. Nothing in a snapshot or a fork carries a credential. - `--image` and `--template` are exclusive. A template's disk was built on its own image and is restored onto it. - Stopping is free and reversible. `tama rm` is neither, and it is the only command that destroys a disk. - `exec`, `ssh` and `prompt` exit with the code of the thing they ran. Branch on the exit status rather than parsing output. - A long job should not depend on your connection. Start it with `nohup ... &`, keep the pid, and follow it with `tama logs`. - Name a machine after what it is for. Names are how every other command finds it. ## Install and sign in curl -fsSL https://tama.computer/install | sh tama login `tama login` opens a browser, writes the credential to `~/.tama/config.yaml`, and generates an SSH key and config of its own. It gives up after five minutes. In CI, adopt a token instead and skip the browser: tama login --token "$TAMA_TOKEN" `--api` and `--console` point the CLI somewhere other than production, and fall back to `TAMA_API` and `TAMA_CONSOLE`. ## Python SDK Use the Python SDK when Python code should control Tama directly instead of shelling out to the CLI. It is synchronous and requires Python 3.10 or newer. Install the package: python -m pip install tama-sdk The distribution is `tama-sdk`; the import is `tama_sdk`. Do not install `tama` or `tama-cli`: those are unrelated PyPI projects. Complete Python-only SDK guide: https://tama.computer/python-sdk.txt Set `TAMA_TOKEN` and call `Tama()`. `TAMA_API_URL` overrides the production gateway. Explicit constructor values win over environment variables. Use a raw token; an optional `Bearer ` prefix is accepted. Never print or log the token: import os from tama_sdk import Tama tama = Tama(api_key=os.environ["TAMA_TOKEN"]) print(tama.identity().account.id) tama.close() This is the safe shape for code that creates a billable machine. `check=True` makes a non-zero remote exit raise `CommandError`. The machine cleanup is separate from closing the client: from tama_sdk import Tama tama = Tama() machine = tama.new(name="python-sdk-example", auto_stop_seconds=900) try: result = machine.exec(["python", "--version"], check=True) print(result.stdout, end="") finally: machine.stop() tama.close() `machine.stop()` snapshots the machine and pauses billing. It is reversible with `machine.start()`. `machine.delete()` and `tama.rm()` permanently destroy the machine; never substitute either one for cleanup unless destruction was explicitly requested. The object model is direct: tama.new(...) # create; waits until ready by default tama.list(all=True) # include stopped machines machine = tama.get("worker") machine.exec(["pytest", "-q"], check=True) machine.keep_active() # explicit idle-policy heartbeat machine.stop() machine.start() tama.snapshots.create(machine.id, label="baseline") tama.snapshots.fork(SNAPSHOT_ID, name="attempt-2") tama.templates.list() tama.secrets.list() tama.tokens.list() tama.files.list(machine.id, "/workspace") Complete `Tama` surface: tama.identity() # account identity tama.usage(days=30) # balance and metered usage tama.offers() # available machine shapes tama.new(..., auto_stop_seconds=900) # create; 15-minute idle auto-stop tama.list(all=False) # list running; all=True includes stopped tama.get(name_or_id) # resolve one tama.keep_active(name_or_id) # refresh the idle deadline tama.rm(name_or_id) # PERMANENT delete tama.stop(name_or_id, no_snapshot=False) tama.start(name_or_id, wait=True, timeout=900) tama.fork(snapshot_id, name="copy", wait=True, timeout=900) tama.exec(name_or_id, command, cwd="", check=False, timeout=None) tama.prompt(name_or_id, prompt, agent="claude", cwd="", detach=False, check=False) tama.logs(session_id) # durable session transcript tama.logs(machine_id, pid) # low-level process logs tama.expose(name_or_id, port) # returns HTTPS URL tama.unexpose(name_or_id, port) tama.ports(name_or_id) # {port: URL or None} tama.desktop(name_or_id) # browser desktop URL tama.terminal(name_or_id) # browser terminal URL tama.enable_ssh(name_or_id, public_key) The `Machine` object has `id`, `name`, `status`, `status_detail`, raw protobuf `data`, and the restore point returned by stop in `snapshot`. Its actions are `refresh`, `exec`, `prompt`, `stop`, `start`, `delete`, `expose`, `unexpose`, `desktop`, `terminal`, and `enable_ssh`. Use these scoped methods once you have a machine. Complete collection surface: tama.machines.create(...) # also get/list/delete/stop/start tama.snapshots.create(machine, label="") tama.snapshots.list(machine="", automatic=False) tama.snapshots.fork(snapshot_id, name="copy") tama.templates.create(machine, name="", description="", public=False) tama.templates.list(); tama.templates.delete(template_id) tama.secrets.set(name, value); tama.secrets.list(); tama.secrets.delete(name) created = tama.tokens.create("ci") # created.secret is returned ONCE tama.tokens.list(); tama.tokens.revoke(created.id) tama.sessions.list(machine); tama.sessions.logs(session_id) tama.files.list(machine, path="~") `tama.start_credit_purchase(amount_cents)` returns a checkout URL and `tama.confirm_credit_purchase(session_id)` refreshes the balance after checkout. Do not attempt payment autonomously: send the URL to the human. `tama.raw` is the generated gRPC stub for forward compatibility; prefer the typed methods above. Fork, expose, then clean up safely: with Tama() as tama: source = tama.get("worker") snapshot = tama.snapshots.create(source.id, label="before-change") if snapshot is None: raise RuntimeError("snapshot was not created") experiment = tama.snapshots.fork(snapshot.id, name="experiment-1") try: experiment.exec( ["sh", "-lc", "python -m http.server 8000 >/tmp/http.log 2>&1 &"], check=True, ) print(experiment.expose(8000)) finally: experiment.stop() # use delete only when destruction is intended Secondary resources are on `tama.snapshots`, `tama.templates`, `tama.secrets`, `tama.tokens`, `tama.sessions`, and `tama.files`. `TamaError` is the base SDK exception; specific subclasses include `AuthenticationError`, `NotFoundError`, `ValidationError`, and `CommandError`. For a detached agent, keep the returned session id and follow its durable transcript: with Tama() as tama: machine = tama.get("worker") session = machine.prompt( "Run the tests, fix failures, and summarize the patch.", agent="codex", detach=True, ) print(session.id) for event in tama.logs(session.id): print(event.data, end="") Read-only RPCs retry short transient failures. Mutations never retry automatically: a timed-out create, exec, or snapshot may already be running server-side. After an ambiguous mutation failure, inspect `tama.list(all=True)` or `tama.get(name)` before trying again. `Tama(timeout=...)` bounds ordinary control RPCs. Long operations such as stop, snapshot, template creation and deletion have no artificial short deadline. `exec(...)` is also unbounded by default; pass `exec(..., timeout=300)` when the command needs an explicit client-side limit. An attached prompt must receive a terminal log event or it raises `ProtocolError` rather than inventing success. `detach=True` returns before an exit code exists, so combining it with `check=True` is rejected. ## TypeScript SDK Use the TypeScript SDK when Node.js or browser code should control Tama directly. It is typed, promise-based, and the Node.js entry requires Node.js 20 or newer. Install it: npm install @tama-computer/sdk@0.1.2 The package is `@tama-computer/sdk`. Browser bundles use `@tama-computer/sdk/browser`. Complete TypeScript-only SDK guide: https://tama.computer/typescript-sdk.txt Node.js reuses the CLI profile selected by `tama login`. In CI, set `TAMA_TOKEN`. `TAMA_API_URL` overrides the production gateway. Explicit constructor values take precedence. Raw tokens and an optional `Bearer ` prefix are accepted: import { Tama } from '@tama-computer/sdk' const tama = new Tama({ apiKey: process.env.TAMA_TOKEN! }) console.log((await tama.identity()).account?.id) Browser code supplies its current access token. An async provider is called for every request, which lets a dashboard rotate credentials without recreating the client: import { Tama } from '@tama-computer/sdk/browser' const tama = new Tama({ apiUrl: 'https://gateway.tama.computer', apiKey: async () => await auth.currentAccessToken(), }) This is the safe shape for code that creates a billable machine. `check: true` makes a non-zero remote exit raise `CommandError`. Stopping the machine is separate from the lifetime of the local client: import { Tama } from '@tama-computer/sdk' const tama = new Tama() const machine = await tama.new({ name: 'typescript-sdk-example', autoStopSeconds: 900 }) try { const result = await machine.exec( ['python', '--version'], { check: true }, ) process.stdout.write(result.stdout) } finally { await machine.stop() } `machine.stop()` snapshots the machine and pauses billing. It is reversible with `machine.start()`. `machine.delete()` and `tama.rm()` permanently destroy the machine; never substitute either one for cleanup unless destruction was explicitly requested. Letting a `Tama` object go out of scope does not stop remote resources. The common object model mirrors Python: await tama.new({ ... }) // create; waits until ready by default await tama.list({ all: true }) // include stopped machines const machine = await tama.get('worker') await machine.exec(['pytest', '-q'], { check: true }) await machine.keepActive() // explicit idle-policy heartbeat await machine.stop() await machine.start() await tama.snapshots.create(machine.id, { label: 'baseline' }) await tama.snapshots.fork(SNAPSHOT_ID, { name: 'attempt-2' }) await tama.templates.list() await tama.secrets.list() await tama.tokens.list() await tama.files.list(machine.id, '/workspace') Complete `Tama` surface: await tama.identity() // account identity await tama.usage(30) // balance and metered usage await tama.offers() // available machine shapes await tama.new({ autoStopSeconds: 900 }) // create; 15-minute idle auto-stop await tama.list({ all: false }) // all:true includes stopped await tama.get(nameOrId) // resolve one await tama.keepActive(nameOrId) // refresh the idle deadline await tama.rm(nameOrId) // PERMANENT delete await tama.stop(nameOrId, { noSnapshot: false }) await tama.start(nameOrId, { wait: true, timeoutMs: 900_000 }) await tama.fork(snapshotId, { name: 'copy', wait: true }) await tama.exec(nameOrId, command, { cwd: '', check: false }) await tama.prompt(nameOrId, prompt, { agent: 'claude', check: false }) tama.logs(sessionId) // durable session transcript tama.logs(machineId, pid) // low-level process logs await tama.expose(nameOrId, port) // returns HTTPS URL await tama.unexpose(nameOrId, port) await tama.ports(nameOrId) // Map await tama.desktop(nameOrId) // browser desktop URL await tama.terminal(nameOrId) // browser terminal URL await tama.enableSsh(nameOrId, publicKey) A `Machine` has `id`, `name`, `status`, `statusDetail`, `autoStopSeconds`, `lastActiveAt`, `autoStopAt`, raw protobuf `data`, and the latest stop restore point in `snapshot`. Its actions are `refresh`, `keepActive`, `exec`, `prompt`, `stop`, `start`, `delete`, `expose`, `unexpose`, `desktop`, `terminal`, and `enableSsh`. Complete collection surface: await tama.machines.create({ ... }) // also get/list/delete/stop/start await tama.snapshots.create('worker', { label: 'baseline' }) await tama.snapshots.list({ machine: 'worker', automatic: false }) await tama.snapshots.fork(snapshotId, { name: 'copy' }) await tama.templates.create('worker', { name: 'base', public: false }) await tama.templates.list(); await tama.templates.delete(templateId) await tama.secrets.set('OPENAI_API_KEY', value) await tama.secrets.list(); await tama.secrets.delete('OPENAI_API_KEY') const created = await tama.tokens.create('ci') // created.secret is returned ONCE await tama.tokens.list(); await tama.tokens.revoke(created.id) await tama.sessions.list('worker'); tama.sessions.logs(sessionId) await tama.files.list('worker', '/workspace') `startCreditPurchase(amountCents)` returns a checkout URL and `confirmCreditPurchase(sessionId)` refreshes the balance after checkout. Send a human to the console instead of automating payment. `tama.raw` is the generated Connect client for forward compatibility; prefer the typed methods above. For a detached agent, keep the session id and follow its durable transcript: const machine = await tama.get('worker') const session = await machine.prompt( 'Run the tests, fix failures, and summarize the patch.', { agent: 'codex', detach: true }, ) for await (const event of tama.logs(session.id)) { process.stdout.write(event.data) } Read-only calls retry short transient failures. Mutations never retry automatically: a timed-out create, exec, or snapshot may already be running server-side. After an ambiguous failure, inspect `tama.list({ all: true })` or `tama.get(name)` before retrying. `timeoutMs` on `new Tama(...)` bounds ordinary control calls. Long operations such as stop, snapshot, template capture, deletion, and exec do not inherit that short deadline; pass `exec(..., { timeoutMs: 300_000 })` for an explicit command limit. An attached prompt must receive a terminal event or it raises `ProtocolError`; a detached prompt cannot use `check: true` because its exit code does not exist yet. ## See what hardware there is tama offers # aliased to `tama gpus` tama offers --json Columns are the card, its rate per hour, the fewest and the most of the card one machine comes with, and the cores and memory the machine gets. Each row is the machine `new` would give you for that card, at the price it is billed. A card with a serverless pool is one card on shared capacity, priced per card-hour, with the size a spec naming it gets; `min gpus` says `1`. A card without one is rented as a whole node, billed whole for as long as your machine holds it, and the size is the whole box, since a rental is sized to the box that turns up whatever `--cpu` and `--memory` said. A card only offered eight to a box is quoted at the box, and `min gpus` says `8`. `--json` adds `cards_on_node`, `node_cpu_millicores`, `node_memory_mb` and `starts_in`, which is `seconds` for a warm worker, `a minute` for a pool with nothing up yet, and `minutes` for a rental that has to be provisioned. ## Take a machine tama new # 1 vCPU, 1024 MiB, name generated for you tama new trainer --gpu H100 # one card, sized for the node it is rented in tama new api --port 3000 --port 8000 # published at create, URLs printed with it tama new box --sync . # contents of the current directory into /workspace tama new box --template h100-baseline # filesystem already in place tama new box --json # the machine as JSON, for scripting Every argument is optional. The flags worth knowing: --gpu a card by name; case does not matter --gpu-count leave at 1 --capacity auto (default), serverless, or on-demand --cpu fractional allowed; 1 is the floor --memory MiB, not GiB; 1024 is the floor --ttl snapshot and stop after this many idle seconds; 0 disables it --port repeatable, published as the machine comes up --env KEY=VALUE repeatable, non-secret --secret repeatable; a workspace secret, injected as that variable --image defaults to the machine base image --dockerfile build this and run it; --context sets the build context --build-secret a secret available only while the Dockerfile builds --template start from a template --desktop include the virtual desktop; these machines resume cold --no-ssh skip SSH setup --sync copy a local directory into /workspace on the way up --json print the machine as JSON Cards include B200, H200, H100, A100-80, RTXPro6000, L40S, RTX5090, A6000, RTX4090, and others such as T4, L4, A10G, V100, GH200 and B300. `tama offers` is the list that is true today. 1 vCPU and 1024 MiB are floors rather than defaults: the service takes the larger of what you asked for and its own minimum. A machine that names a card is sized against the node that card is rented in instead of that floor, so `--gpu H100` comes up with enough cores and memory to keep the card fed. `--cpu` and `--memory` still win where you pass them. Ports asked for at create are re-published on every restore and they follow a fork. A machine has no idle timeout unless you ask for one (60 seconds minimum), so `--ttl` is worth passing to anything you might forget about. Tama snapshots and uses the ordinary stop path when the deadline expires. CLI/SDK calls, SSH/tunnels, agent sessions, and published HTTP/WebSocket traffic refresh it. Background compute with no Tama-visible traffic can look idle; disable the policy or call `keep_active()` / `keepActive()` from its controller. ## Get code and data in and out tama new box --sync . # at create tama sync box . # again, later tama scp ./dataset.csv box:/workspace/dataset.csv tama scp -r box:/workspace/out ./out `--sync` and `tama sync` copy a directory's *contents* into `/workspace`, so `.` puts a project at `/workspace` rather than at `/workspace/.`. ## Run things tama ssh box # interactive shell, as the tama user tama ssh box nvidia-smi # one command, over SSH tama exec box python train.py # streams output, exits with its code tama exec --cwd /workspace/api box pytest -q tama terminal box # a shell in the browser; --url to just print it tama desktop box # the virtual desktop, if created with --desktop For anything that outlives your shell, background it inside the machine and keep the pid: pid=$(tama exec box sh -c 'nohup python train.py > /workspace/train.log 2>&1 & echo $!') tama logs box "$pid" # follow it; Ctrl-C stops watching, not the job ## Run an agent inside the machine tama secret set ANTHROPIC_API_KEY # or OPENAI_API_KEY / CODEX_API_KEY for codex tama prompt box "Profile train.py and cut step time by a third." tama prompt box "..." --agent codex --cwd /workspace/api tama prompt box "..." --detach # prints a session id tama logs SESSION_ID The service starts the agent inside the machine and the CLI only follows its output, so disconnecting stops the watching and not the work. Attached, `tama prompt` exits with the agent's exit code. It does not print a process id, so an attached run whose output you stop collecting cannot be reattached — tell the agent to write results to a file under `/workspace` and read that file later. Claude and Codex are both in the base image. Claude reads `ANTHROPIC_API_KEY`; Codex reads `CODEX_API_KEY` or `OPENAI_API_KEY`. A missing key becomes a warning in the transcript rather than an error at start. ## Reach a port tama expose box 8000 # a URL anyone with the link can open tama ports box --json tama unexpose box 8000 tama port-forward box 8000 # local only, over the authenticated channel tama port-forward box 9000:8000 # LOCAL:REMOTE ## Keep state, and run many attempts tama snapshot create box # prints the snapshot id; does not stop anything tama snapshot create box -l baseline tama snapshot list box --json tama stop box # snapshots, then pauses billing tama stop box --no-snapshot # faster, and unresumable tama start box # warm if memory was caught, else a cold boot tama fork box attempt-2 # a second machine from the same snapshot tama list --all # stopped ones are left out until you ask tama rm box --yes A warm restore comes back in about a second with the processes that were running still running, GPU memory and CUDA context included. A machine created with `--desktop` never captures memory, so it always resumes cold. A fork boots cold from a copy of the snapshot's disk, which is what makes attempts comparable: no attempt inherits anything from another, and neither can write over the other. `tama fork` takes a snapshot id, or a machine name to fork that machine's most recent snapshot — automatic ones included. It prints the new machine's name on stdout, so `m=$(tama fork base)` is all a loop needs. ## Pin a starting point tama template create box h100-baseline -d "torch 2.6, dataset staged" tama template create box shared-baseline --public tama template list --json tama template show h100-baseline --json tama template rm h100-baseline A template is a launch recipe: the machine's image, its resources and its durable disk, plus the memory checkpoint where one was caught and the launch is in the same workspace. `tama template show --json` is the one to read before choosing a starting point — it states the card and memory it needs, whether a memory checkpoint exists, which environment variables and secret names it wants, and a command that launches it. Secret values are never included. Deleting a template leaves machines made from it alone. ## Build a custom image tama image build . # CONTEXT/Dockerfile, streamed tama image build . -f docker/Dockerfile --secret HF_TOKEN tama new box --dockerfile Dockerfile --build-secret HF_TOKEN Build once and reuse the image, or hand `tama new` a Dockerfile and let it build on the way up. `--secret` and `--build-secret` expose a workspace secret to the build only; it is not in the resulting image. ## Secrets, tokens, and local state tama secret set HF_TOKEN # reads $HF_TOKEN, else prompts, else stdin tama secret list tama secret rm HF_TOKEN tama tokens create --name ci # printed once, never stored in full tama tokens list --json tama tokens revoke TOKEN_ID Everything the CLI keeps is under `~/.tama` and is regenerated by signing in again: the credential, an ed25519 key of its own, an SSH config and a known_hosts. `TAMA_HOME` moves that directory, which is how a CI job keeps its state out of the invoking user's. `--json` is available on `new`, `list`, `offers`, `snapshot list`, `template list`, `template show`, `fork`, `ports`, `secret list`, `tokens list` and `image build`. The rest print one line or stream a process. ## Tasks ### Fine-tune a model on a GPU tama offers tama secret set HF_TOKEN tama new tuner --gpu H100 --sync . --secret HF_TOKEN --ttl 7200 tama exec tuner pip install -r requirements.txt pid=$(tama exec tuner sh -c 'nohup python train.py > /workspace/train.log 2>&1 & echo $!') tama logs tuner "$pid" tama scp -r tuner:/workspace/out ./out tama stop tuner `--ttl 7200` is insurance against forgetting it. Stopping keeps the disk and the weights on it for nothing, so the next round starts from a machine that is already set up. ### Find out where inference time is going tama new perf --gpu H100 --sync . tama exec perf python bench.py --save /workspace/baseline.json tama snapshot create perf -l baseline tama prompt perf "Read baseline.json, profile serve.py, and cut p50 latency. Re-run bench.py and write the comparison to /workspace/report.md." tama exec perf cat /workspace/report.md Snapshot the baseline before the agent touches anything: `tama start` after a stop, or `tama fork perf`, both come back to a machine whose numbers are still comparable. ### Run a sweep, all at once tama new base --gpu L40S --sync . tama exec base pip install -r requirements.txt tama template create base sweep-base -d "deps installed, dataset staged" for lr in 1e-4 2e-4 4e-4; do m=$(tama fork base "exp-$lr") tama exec "$m" sh -c "nohup python train.py --lr $lr > /workspace/run.log 2>&1 &" done tama list for m in $(tama list --json | jq -r '.[].name | select(startswith("exp-"))'); do tama exec "$m" tail -5 /workspace/run.log tama stop "$m" done Every fork starts from the same disk, on its own card, and each one is billed only while it runs. Pinning `sweep-base` as a template means the next sweep skips the setup entirely: `tama new --template sweep-base`. ### Leave something running overnight tama new long --gpu H100 --sync . pid=$(tama exec long sh -c 'nohup python sweep.py > /workspace/sweep.log 2>&1 & echo $!') # close the laptop tama logs long "$pid" # or: tama exec long tail -100 /workspace/sweep.log tama exec long pgrep -f sweep.py # empty output means it finished tama stop long Nothing about the job depends on your shell. Do not pass `--ttl` to a machine that will look idle while it works. ### Serve something on a URL tama new demo --port 8000 --sync . tama exec demo sh -c 'nohup python -m uvicorn app:main --host 0.0.0.0 --port 8000 > /workspace/serve.log 2>&1 &' tama ports demo tama expose demo 8001 # a port you did not ask for at create tama port-forward demo 8000 # keep it private instead Bind to `0.0.0.0`, not `127.0.0.1`, or nothing outside the machine can reach it. ### Run a job in CI tama login --token "$TAMA_TOKEN" machine=$(tama new --gpu H100 --sync . --json | jq -r .name) trap 'tama rm --yes "$machine"' EXIT tama exec "$machine" python bench.py The `trap` is the part to copy: `rm` in a trap is what keeps a failed job from leaving a machine running. Set `TAMA_HOME` to a directory inside the workspace to keep the runner's credential out of the invoking user's home. ### Hand a machine to someone else tama template create tuner krea2-ft --public -d "Krea 2 fine-tuning workspace" tama template show krea2-ft A public template is listed and launchable by every account, and its disk snapshot is published with it. Secret bindings are stripped and a cross-workspace launch boots cold, so process memory never leaves the workspace it was captured in. ### Clean up tama list --all tama stop box # keep the disk, stop the meter tama rm box --yes # destroy it tama snapshot list box # what a start or a fork would come back to