Quickstart — your first pipeline¶
TOVIO runs CI/CD from the workflow definitions in your repo. You can write those definitions two ways, and both compile to the same job DAG that TOVIO schedules and runs:
- Path A — YAML. Drop a
.github/workflows/*.ymlin your repo. Nothing else to configure — TOVIO reads it straight from your working copy. - Path B — config-as-code. Write the pipeline as a typed
@tovio/pipelineprogram, then compile it withtovio ci compileinto a committed lockfile and a generated.github/workflows/*.yml. No pipeline code ever runs on the Forge — it schedules the generated YAML, exactly as in Path A.
This guide builds the identical three-job pipeline — lint → build → test (matrix) — both ways, so you can see they're equivalent and pick whichever you prefer.
Before you expect a green check: a Forge ships with no executor connected, so the pipeline you define here is discovered, classified, and scheduled as checks, but nothing runs a job until you connect a customer-operated runner — every scheduled check resolves failure instead. That only blocks a land where the check is required, which comes from a lane's protection settings or a change-control rule, never from the mere fact that CI posted it. So keep CI out of your required_checks until a runner is connected. Read the notice at the top of the CI/CD overview first.
Prerequisites¶
- A TOVIO repo (
tovio init, or a clone). - Path B only: Node.js ≥ 18 on your
PATH.tovio ci compileruns your pipeline program withnode; if it's missing you'll get a clearTVO-CI-001telling you to install it. - Path B only: the
tovio-node-sdkpackage resolvable from your pipeline program. It is not published to a public registry yet, so today that means the in-repo package — see the SDK reference.
Path A — YAML (the fast on-ramp)¶
Create .github/workflows/ci.yml:
name: CI
on: [pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cargo clippy --all-targets -- -D warnings
- run: cargo fmt --check
build:
needs: [lint]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cargo build --release
test:
needs: [build]
runs-on: ubuntu-latest
strategy:
matrix:
node: ["18", "20", "22"]
steps:
- uses: actions/checkout@v4
- run: cargo test --workspace
That's the whole setup. There's no separate registration step — TOVIO discovers every .github/workflows/*.yml in your working copy automatically. The on: [pull_request] trigger maps onto TOVIO's change events: the jobs are scheduled as checks when a proposal is opened or amended. (push is mapped onto the land event, but nothing schedules a run for a land today — see YAML & compatibility — so keep pull_request on any workflow you expect to run.)
The test job's strategy.matrix fans it out into three parallel instances — one per node value. Note the values are quoted strings ("18", not 18); this matters more for versions like "3.10", which would otherwise lose its trailing zero.
Now see how TOVIO will run it. From the repo root:
tovio ci check prints a per-pipeline compat report — it classifies every job and step as native (a run: step), shimmed (a first-party action backed by a TOVIO primitive, like actions/checkout@v4), container (needs a Docker-in-Docker runner), or unsupported (with the reason). It's read-only and never fails your build; it's how you see exactly what will and won't run before the pipeline first runs:
✓ CI (.github/workflows/ci.yml)
[ok ] job lint
shimmed step 0 [actions/checkout]
native step 1
native step 2
[ok ] job build
shimmed step 0 [actions/checkout]
native step 1
[ok ] job test
· fans out via `strategy.matrix` into multiple job instances
shimmed step 0 [actions/checkout]
native step 1
A step is labelled by its name: when it declares one, and step <index> when it doesn't — the three jobs above are all unnamed steps. A job's advisory notes (the matrix line on test) print above its steps.
Add --json for a machine-stable report you can gate on in tooling.
Path B — config-as-code with the SDK¶
Prefer typed, composable pipelines over YAML? Write the same DAG as a @tovio/pipeline program. Create pipelines/ci.pipeline.mjs:
import { pipeline, run, uses, emit } from "tovio-node-sdk/pipeline";
// A shared checkout step, reused across jobs (the kind of DRY YAML can't express).
const checkout = uses("actions/checkout@v4");
emit(
pipeline("CI")
.on("pull_request")
.job("lint", {
runsOn: "ubuntu-latest",
steps: [checkout, run("cargo clippy --all-targets -- -D warnings"), run("cargo fmt --check")],
})
.job("build", {
needs: ["lint"],
runsOn: "ubuntu-latest",
steps: [checkout, run("cargo build --release")],
})
.job("test", {
needs: ["build"],
runsOn: "ubuntu-latest",
matrix: { node: ["18", "20", "22"] }, // axis values MUST be strings
steps: [checkout, run("cargo test --workspace")],
}),
);
A few things to know about the API:
- Import the builder from
tovio-node-sdk/pipeline—pipeline,run,uses, andemit. pipeline(name).on(...).job(id, spec)chains fluently. A step is arun(command, opts?), auses(action, opts?), or a bare string (shorthand for arun);optscarries a stepname, ashell(forrun), an action'swithinputs (foruses), and anenvmap, all as string values. Jobspecfields areneeds,runsOn,matrix,environment,ifCond,name, andsteps— plus the snake/GitHub aliasesruns_onandif, which the builder accepts for the same two fields.- Matrix axis values must be strings — a JS number like
3.10is silently3.1, which would run the wrong version. The SDK throws aPipelineErrorif you pass a non-string. - Finish with
emit(pipeline). That prints the canonical workflow JSON thattovio ci compilereads. Nothing is written to disk by the program itself.
Now compile it. To make it actually run, emit both the lockfile and a real workflow — the Forge schedules from .github/workflows/*.yml, not the lockfile:
This runs your program under node, compiles the emitted workflow (validate + content-address) through the same pure core that YAML goes through, writes the deterministic lockfile, and serializes the same DAG to a runnable .github/workflows/ci.yml (--fix-ignore un-ignores .github/, which TOVIO ignores by default):
✓ re-included `.github/` in .tovioignore
✓ compiled CI · 3 jobs
lockfile → pipelines/ci.pipeline.lock.json (blake3:9b2f…)
workflow → .github/workflows/ci.yml
(The first line is what --fix-ignore did; on a repo that already re-includes .github/ it doesn't appear.)
The generated .github/workflows/ci.yml is the artifact TOVIO registers and runs — a normal GitHub-Actions workflow, the same lint → build → test DAG as Path A, flowing through the identical YAML path. The lockfile is the deterministic staleness record that --check reads. (Author your .on(...) with GitHub event names like pull_request/push — the SDK rejects the dotted change.* names, which would generate a workflow that never runs.)
Commit the source, the lockfile, and the generated workflow:
The .mjs is what you edit; the .yml is what runs; the .lock.json is the record --check trusts. TOVIO re-hashes the lockfile on read, so a hand-edited lockfile whose body doesn't match its stored content address is rejected.
Prefer not to re-run compile by hand? Turn on commit-time regeneration and TOVIO regenerates the .yml for you on every commit (fail-closed — a broken pipeline aborts the commit):
To keep source and generated artifacts in sync across your team, add a compile-check to CI. It recompiles from source and byte-compares the committed lockfile and .yml — a stale or missing artifact is an error, and it writes nothing:
✓ pipelines/ci.pipeline.lock.json is up to date (blake3:9b2f…)
✓ .github/workflows/ci.yml is up to date
If someone edits the .mjs without regenerating, --check fails with TVO-CI-001 and points them at tovio ci compile.
Finally, confirm the compiled pipeline classifies the same way YAML does:
tovio ci check reads committed *.pipeline.lock.json lockfiles alongside your YAML and classifies them identically — a lockfile is tagged · config-as-code so you can tell the two front-ends apart at a glance:
✓ CI (pipelines/ci.pipeline.lock.json) · config-as-code
[ok ] job lint
shimmed step 0 [actions/checkout]
native step 1
native step 2
...
See it in the web UI¶
Once your pipeline is committed, the runs, job DAG, logs, and deployment gates show up in the repo's CI/CD section of the TOVIO web UI — see the web UI section of this guide.
Next steps¶
- Full
@tovio/pipelineAPI — everyjobspec field,onSchedule, matrixaxes, step options: see the SDK reference section. - Every
tovio ciflag (--out,--check,--name,--json) and its exact behavior: see the CLI reference section.
Last reviewed September 9, 2026
Suggest an improvement to this page Not for security reports — see disclosure