Runs & pipelines

layered config → an immutable resolved run, and multi-stage DAGs of runs

Three 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:

platform
deployment-wide defaults (lowest precedence)
org
per-tenant overlay
project
per-project overlay
user
the caller’s own values (highest precedence — wins)

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 set allow_failure: true, in which case its dependents still run.
  • A stage that can’t even be admitted (unknown stack, quota 429) is recorded failed so 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.

Pipelines are enabled per deployment — a submit against a deploy without the pipeline store returns 503 pipelines disabled. Reads and validation always work.