CLI¶
ScriptHut ships a gh-style CLI for triggering workflows, managing stacks, firing one-off tasks, inspecting runs, and tailing logs without opening the web UI. The single binary is the same scripthut entry point that runs the server — when called with a subcommand (workflow, run, backend, project, stack, task, agent) it dispatches to the CLI instead.
The CLI is designed to feel native from inside a project directory: it walks up from the current working directory looking for a scripthut.yaml, merges it with your user-global config (~/.config/scripthut/scripthut.yaml), and gives every subcommand the right context automatically. See From a project for the full layered-config model and a worked example.
Transports — local vs remote¶
The CLI talks to your workflows through one of two clients, picked automatically by the server-resolution chain:
--server <url>argument on the command (overrides everything; passlocalto force local mode)SCRIPTHUT_SERVERenvironment variablesettings.cli_serverinscripthut.yaml- None of the above → local mode
| Transport | Picked when | Behavior |
|---|---|---|
RemoteClient (HTTP) |
a server URL is resolved | Calls the running server's /api/v1 endpoints via httpx. The server submits and tracks the work; the CLI just queries it. |
LocalClient (in-process) |
no server URL anywhere | Boots a Runtime in-process — same backend SSH connections, storage, and RunManager the server uses. No web server required. |
So scripthut workflow run train on a laptop with no config will boot connections, submit the run, and exit. The same command pointed at a running server (--server https://scripthut.team.example) hits its API instead.
Set the default server once in scripthut.yaml so day-to-day CLI use is point-free:
Global flags¶
Every subcommand accepts these:
| Flag | Description |
|---|---|
--server <url> |
Server URL to target. Pass local to force local mode. |
--config <path>, -c <path> |
Path to scripthut.yaml. Loads exactly that file and skips the layered discovery. Useful in tests and one-off scripts. |
--json |
Print machine-readable JSON instead of a formatted table (where supported). |
Config discovery (without --config)¶
When you don't pass --config, the CLI loads up to two files and merges them:
- User-global:
~/.config/scripthut/scripthut.yaml(or~/.scripthut.yaml). - Project-local: the first
scripthut.yamlfound by walking up from$PWD.
Project-local files may only define stacks, workflows, projects, env, env_groups. Infrastructure fields (backends, sources, settings, pricing) come from the global file and are rejected in a project-local file with a clear error. Full details and examples in From a project.
workflow — manage workflows¶
scripthut workflow list # show all workflows and projects
scripthut workflow view <name> # dry-run preview the tasks
scripthut workflow view <name> --backend <b> # preview against a different backend
scripthut workflow run <name> # submit a run
scripthut workflow run <name> --backend <b> # submit, overriding the backend
scripthut workflow run <sflow.json> --project <name> # submit from a git project
workflow run prints the new run's ID and a link/path you can pass straight to scripthut run watch <id>.
run — inspect and control runs¶
scripthut run list # 20 most recent runs
scripthut run list --limit 100 # last 100
scripthut run view <id> # task table + status counts
scripthut run watch <id> # poll until the run terminates
scripthut run watch <id> --exit-status # exit non-zero on FAILED/CANCELLED
scripthut run watch <id> --interval 2 # tighter polling
scripthut run cancel <id> # cancel a running run
scripthut run rerun <id> # re-execute as a NEW run
scripthut run rerun <id> --in-place # reset and resubmit the same run
scripthut run logs <id> <task> # stdout for one task
scripthut run logs <id> <task> --error # stderr
scripthut run logs <id> <task> --tail 100 # only the last 100 lines
scripthut run logs <id> <task> --follow # tail until the task ends
watch --exit-status is the CI-friendly form: it returns 0 only when every task in the run completes successfully.
backend — inspect configured backends¶
Useful when a workflow hangs at submission to confirm the right backend is actually reachable.
agent — coding-agent helpers¶
agent prompt emits a markdown document that teaches a coding agent how to use this scripthut from the current project. It's a mix of a static reference (CLI patterns, TaskDefinition shape, exit codes, common gotchas) and a live inventory (the backends, stacks, and workflows the layered config currently exposes) so the agent's suggestions reference real names rather than placeholders.
Typical use is to pipe it into the agent's context:
# Capture once and feed to your agent of choice
scripthut agent prompt > .agent-brief.md
# Or pipe through xclip / pbcopy / etc.
scripthut agent prompt | pbcopy
Re-run whenever the user adds a backend, stack, or workflow — the inventory is read fresh each invocation.
task — submit ad-hoc tasks¶
Sometimes you don't want to commit a task definition to a git repo or wire a workflow generator — you just want to fire a single command at a configured backend. task run is the shortest path to that, and is the entry point that's friendliest for coding agents.
# Simplest form: a one-line command on a configured backend.
scripthut task run "python -c 'print(2+2)'" --backend mercury-nb
# Run a local script on the backend WITHOUT staging files first
# (script is base64-embedded into the task command).
scripthut task run --inline-script ./probe.py \
--backend mercury-nb --cpus 1 --memory 1G --time 0:05:00
# With explicit resource shape.
scripthut task run "python train.py" \
--backend mercury-nb \
--cpus 8 --memory 32G --time 4:00:00 \
--partition gpu --gres gpu:1 \
--working-dir /scratch/me/repo \
--env CUDA_VISIBLE_DEVICES=0 --env WANDB_PROJECT=demo
# Feed a full TaskDefinition JSON via stdin — handy for agents.
echo '{
"id": "exp-42",
"name": "exp 42",
"command": "python train.py --lr 1e-3",
"cpus": 8,
"partition": "gpu",
"gres": "gpu:1"
}' | scripthut task run --from-stdin --backend mercury-nb --json
# Or from a JSON file (CLI flags still override individual fields).
scripthut task run --from-file experiment.json --backend mercury-nb --cpus 16
# Verify the assembled task body without submitting.
scripthut task run "echo hi" --backend mercury-nb --dry-run
What it does¶
task run builds a single TaskDefinition (the same shape used by workflow generators) and submits it as a one-item run. The run shows up in the dashboard and in scripthut run list like any other; behind the scenes its workflow_name is _adhoc/<task-id> (override with --run-name <label> if you want something more memorable).
Input modes (mutually exclusive)¶
| Source | When it's used |
|---|---|
command (positional) |
Genuine one-liners. If you find yourself quoting a multi-line script, switch to --inline-script. |
--inline-script <local-path> |
A local script file you want to run on the backend without copying or git-committing it first. ScriptHut base64-embeds the file into the task command and the backend decodes + executes it. Files without a #! line get #!/bin/bash prepended. Best for files up to a few hundred KB. |
--from-stdin |
Pipe a full TaskDefinition JSON. Most reliable for agents that construct the payload programmatically. |
--from-file <path> |
Same JSON shape, from a file. CLI flags layered on top still override individual fields. |
Passing more than one of these is an error — silent precedence would mean you thought you were submitting one thing and you weren't.
--dry-run prints the assembled {"task": ..., "backend": ...} and exits without touching any backend — let an agent verify the payload before committing.
Default id and name¶
If you don't pass --id, the task gets adhoc-<8-hex-chars> derived from the command and a nanosecond timestamp. Two consecutive runs with the same command get different ids, so they don't collide on disk. --name defaults to whatever --id resolves to.
Output¶
Without --json, prints a single human-readable line and the command to inspect the run:
With --json, prints the full run summary:
{
"id": "a1b2c3d4",
"workflow_name": "_adhoc/adhoc-1f2e3d4a",
"backend_name": "mercury-nb",
"task_count": 1,
"submitted_count": 1,
"status_counts": {"submitted": 1},
...
}
The shape matches workflow run --json, so an agent can pipe straight into scripthut run watch "$ID" or other automation built around that contract.
Use with stacks¶
Tasks that need a particular runtime should be paired with a stack:
scripthut stack install julia-1.11 --backend mercury-nb
scripthut task run "julia --project=. scripts/run.jl" \
--backend mercury-nb \
--working-dir /home/me/balke-jmp \
--cpus 16 --memory 64G
Stacks are installed once; ad-hoc tasks reference them via their resolved STACK_DIR in the command or working directory.
Notes for coding agents¶
- The CLI is the supported entry point — there's no separate "agent API." The
--jsonflag plus stable exit codes (0submitted,1error) are the contract. - Default pattern: write your script to a local file → submit with
--inline-script <path>→ captureidfrom--json→ pollscripthut run view <id> --json. No file staging, no scp, no git commit needed for small scripts. - For large or multi-file work, fall back to a workflow with a git repo —
--inline-scriptis for "run this file" not "run my whole repo." --dry-runis a good safety check before submission; pair it withscripthut backend listto verify the target backend is reachable.- The HTTP form is
POST /api/v1/tasks/runwith a body of{"task": {...}, "backend": "...", "run_name": "..."}— use it directly if you're talking to a running scripthut server (setSCRIPTHUT_SERVERand the CLI picks remote mode automatically). - Tasks submitted this way still respect the layered config —
working_dirresolution, env rules, partition mapping, and account selection fromscripthut.yamlall apply. - For a self-contained briefing you can paste into your context window, run
scripthut agent prompt(see theagentsection above).
stack — manage reusable software stacks¶
A stack is a software environment (Python venv, Julia depot, Conda env, …) ScriptHut installs once per backend and reuses across runs. The CLI is the lifecycle interface — see Stacks for the model and YAML schema.
scripthut stack list # configured stacks (no SSH)
scripthut stack check [<name>] [--backend X] # per-backend state table
scripthut stack install <name> [--backend X] [--rebuild]
scripthut stack delete <name> [--backend X]
Each command opens an SSH connection per (stack × selected backend). Without --backend, the command iterates every backend the stack declares (or every SSH-capable backend if the stack's backends: list is empty). Non-SSH backends (Batch, EC2) are silently skipped for now.
checkexits non-zero if any stack on any selected backend is notready— handy as a CI gate before submitting work.installis idempotent: a no-op when the stack is already ready at the current hash.--rebuildforces a fresh build even when the hash matches.deleteremoves the entire<cache_dir>/<name>/directory on the backend (every hash, not just the current one). A subsequentinstallrebuilds from scratch.
Example session from inside a project:
cd ~/git/my-project
scripthut stack check julia-1.11 # is it built on every backend?
scripthut stack install julia-1.11 # build any that aren't
scripthut workflow run grid-search # submit work that relies on the stack
project — inspect git projects¶
scripthut project list # all configured projects
scripthut project view <name> # show sflow.json files + their workflows
A "project" is a git repo on a backend that contains one or more sflow.json workflow files; project view lists them so you can pick one to feed into workflow run --project.
Common patterns¶
Submit a workflow and follow it to completion¶
RUN_ID=$(scripthut workflow run train --json | jq -r .id)
scripthut run watch "$RUN_ID" --exit-status
The two commands compose because --json on workflow run yields a stable shape ({"id": "...", "items": N, ...}).
Tail one task's stderr until it finishes¶
--follow reads the file once it appears on the backend, then polls until the task moves to a terminal state.
Inspect the resolved environment for a task¶
The Env tab in the web UI shows resolved env with per-key provenance. The same data is available via the /runs/{id}/tasks/{task_id}/env endpoint, which RemoteClient can hit directly:
See Environments for the full env-rule resolution model that produces these values.
CI / automation¶
In CI you usually want:
export SCRIPTHUT_SERVER="https://scripthut.team.example"
RUN_ID=$(scripthut workflow run nightly-eval --json | jq -r .id)
scripthut run watch "$RUN_ID" --exit-status
Setting SCRIPTHUT_SERVER once at the top means the rest of the script reads naturally and scripthut never accidentally drops into local mode.
Exit codes¶
| Code | Meaning |
|---|---|
0 |
Command succeeded (and for run watch --exit-status, the run completed successfully) |
1 |
Command-level error: bad arguments, missing workflow/run, server unreachable, etc. |
2 |
(run watch --exit-status only) The run terminated in a non-success state (FAILED / CANCELLED) |
Without --exit-status, run watch always returns 0 once the run reaches a terminal state — the watch itself succeeded, even if the work didn't.