Runs & pipelines
layered config → an immutable resolved run, and multi-stage DAGs of runsThree ways to launch, one contract
Everything you launch is a run — one execution of your code on a base stack, obeying the run contract. There are three layers of surface, each a superset of the one before:
1 · POST /v1/jobs — the raw submit. You hand over the fully-resolved spec (stack, command, config, resources).
2 · POST /v1/runs — a RunSpec: the same spec plus four config layers that fold together in a fixed precedence. The server resolves them into an immutable snapshot with per-key provenance, then submits it. run.id == job.id.
3 · POST /v1/pipelines — a DAG of runs: each stage is a RunSpec gated on the ones it needs.
A plain /v1/jobs submit with no layers behaves identically to a run with only a user layer — the surfaces stack cleanly.
RunSpec: four config layers
A RunSpec carries its config as up to four named overlays that merge low → high precedence:
The fold records which layer last set each key (provenance) and the per-layer diff (what each layer contributed, and which of its keys a higher layer shadowed). A flat top-level config is treated as the user layer, so a config-only body still resolves to exactly that config.
Resolution is pure + deterministic: the same RunSpec always resolves to byte-identical JSON. The resolved snapshot rides the job, so a run is reproducible from its spec.
Preview a resolution (dry-run)
POST /v1/runs/resolve folds the layers and returns the effective config, per-key provenance, and the per-layer diff — without enqueuing anything. This is what a UI shows before launch: “what each layer contributed and what will actually run”.
curl -X POST $BASE/v1/runs/resolve \
-H "Authorization: Bearer slt_…" -H "content-type: application/json" \
-d '{
"stack": "openfoam",
"layers": {
"platform": { "mesh_level": "coarse", "cores": 4 },
"org": { "cores": 8 },
"user": { "airspeed_ms": 40, "mesh_level": "fine" }
}
}'Response — provenance names the winning layer per key; layers is the per-layer contribution (overridden lists keys a higher layer shadowed):
{
"resolved": {
"stack": "openfoam",
"resources": { "cpu_cores": 0, "ram_gb": 0, "gpu_count": 0, "timeout_sec": 0 },
"config": { "mesh_level": "fine", "cores": 8, "airspeed_ms": 40 },
"provenance": { "mesh_level": "user", "cores": "org", "airspeed_ms": "user" },
"layers": [
{ "layer": "platform", "set": {}, "overridden": ["cores", "mesh_level"] },
{ "layer": "org", "set": { "cores": 8 } },
{ "layer": "project", "set": {} },
{ "layer": "user", "set": { "airspeed_ms": 40, "mesh_level": "fine" } }
]
},
"diff": [ … ], // convenience alias of resolved.layers
"validation": { "ok": true, "errors": [], "warnings": [] }
}POST /v1/runs/validate is the lighter sibling — it resolves then checks the effective config against the stack manifest and returns { stack, ok, errors, warnings }, nothing enqueued.
Create a run
POST /v1/runs resolves the RunSpec, folds the effective config into the spec, and submits it through the same admission / tenancy / quota pipeline as /v1/jobs. Returns 202 with the job view — and run.id == job.id.
curl -X POST $BASE/v1/runs \
-H "Authorization: Bearer slt_…" -H "content-type: application/json" \
-d '{
"stack": "openfoam",
"project": "wing-study",
"layers": {
"project": { "mesh_level": "medium" },
"user": { "geometry": "wing", "angle_of_attack_deg": 5, "airspeed_ms": 40 }
},
"limits": { "cpu_cores": 8, "timeout_sec": 3600 }
}'Read it back with GET /v1/runs/{id} — the job view plus the immutable run snapshot (config + provenance + layers) and a single-stage stages stub (a plain run is exactly one stage). GET /v1/runs lists your org’s run history (each row carries run_id + has_runspec), filterable by ?stack/?project/?phase with the same cursor pagination as the jobs list.
Pass ?embed=1 to get an embed grant back alongside the run — the LMS one-shot flow. The Idempotency-Key header is honored, so a retried create returns the same run.
Pipelines: a DAG of runs
A pipeline is a multi-stage DAG where each stage is a full RunSpec (stack / command / config / layers / limits) gated on the stages it needs. Root stages (no deps) launch immediately; a stage starts once all its dependencies have succeeded.
- Each stage is a normal admitted run — it inherits the launcher’s org / scopes / quota, so a stage is quota’d and tenanted exactly like a single run.
- Fail-fast: when a stage fails, its dependents are marked
skipped— unless the failed stage setallow_failure: true, in which case its dependents still run. - A stage that can’t even be admitted (unknown stack, quota
429) is recordedfailedso its dependents fail-fast rather than hang.
curl -X POST $BASE/v1/pipelines \
-H "Authorization: Bearer slt_…" -H "content-type: application/json" \
-d '{
"name": "mesh-then-solve",
"project": "wing-study",
"stages": [
{ "name": "coarse", "stack": "openfoam",
"config": { "mesh_level": "coarse", "airspeed_ms": 40 } },
{ "name": "fine", "stack": "openfoam", "needs": ["coarse"],
"config": { "mesh_level": "fine", "airspeed_ms": 40 } },
{ "name": "report", "stack": "python-cuda", "needs": ["fine"],
"allow_failure": true, "command": ["python", "report.py"] }
]
}'Returns 202 with the pipeline run (id plr_…), its status, a progress rollup, and the stages array — each stage’s phase, job_id (once launched), and final_metrics. Poll GET /v1/pipelines/{id} to watch it advance; POST /v1/pipelines/{id}/cancel stops every in-flight stage.
Validate a pipeline first
POST /v1/pipelines/validate checks the DAG topology (cycles / unknown deps / duplicate names) and each stage’s RunSpec against its stack manifest — always 200 with a structured report, nothing enqueued. On submit, a DAG error is a 400 and a stage-config error is a 422, both before anything runs.
curl -X POST $BASE/v1/pipelines/validate \
-H "Authorization: Bearer slt_…" -H "content-type: application/json" \
-d '{ "stages": [ { "name": "a", "stack": "mujoco" },
{ "name": "b", "stack": "mujoco", "needs": ["a"] } ] }'
# → { "ok": true, "errors": [], "stages": [ { "name": "a", "validation": {…} }, … ] }You can also fire a pipeline on a git push — attach a pipeline_on_push trigger to a git connection and every stage runs against the pushed commit.
503 pipelines disabled. Reads and validation always work.