Skip to content

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/*.yml in 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/pipeline program, then compile it with tovio ci compile into 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 compile runs your pipeline program with node; if it's missing you'll get a clear TVO-CI-001 telling you to install it.
  • Path B only: the tovio-node-sdk package 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

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, and emit.
  • pipeline(name).on(...).job(id, spec) chains fluently. A step is a run(command, opts?), a uses(action, opts?), or a bare string (shorthand for a run); opts carries a step name, a shell (for run), an action's with inputs (for uses), and an env map, all as string values. Job spec fields are needs, runsOn, matrix, environment, ifCond, name, and steps — plus the snake/GitHub aliases runs_on and if, which the builder accepts for the same two fields.
  • Matrix axis values must be strings — a JS number like 3.10 is silently 3.1, which would run the wrong version. The SDK throws a PipelineError if you pass a non-string.
  • Finish with emit(pipeline). That prints the canonical workflow JSON that tovio ci compile reads. 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:

tovio ci compile pipelines/ci.pipeline.mjs --emit both --fix-ignore

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:

tovio commit -m "Add CI pipeline (config-as-code)"

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

tovio config set ci.generate on

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:

tovio ci compile pipelines/ci.pipeline.mjs --emit both --check
✓ 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

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/pipeline API — every job spec field, onSchedule, matrix axes, step options: see the SDK reference section.
  • Every tovio ci flag (--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