KDL Pipeline Definition Language
This document describes the design of a KDL-based language to define and modify Xvc pipelines as text, and how it maps onto the existing pipeline machinery.
Motivation
Today a pipeline is built imperatively, one CLI invocation at a time (xvc pipeline step new, step dependency, step output, ...), or declaratively via
xvc pipeline export / import with JSON or YAML. The JSON/YAML schema is a
direct serialization of internal structures: it leaks digest fields, is verbose,
and doesn't express the graph users think in.
KDL is a small document language designed for exactly this kind of
configuration. The KDL pipeline definition is a third, human-first format for
export / import that describes a pipeline the way users reason about it:
- Nodes are dependencies — the data the pipeline observes: files, globs, parameters, regex/line fragments, URLs, database queries, generic commands.
- Edges are commands — a step connects the nodes it consumes to the outputs it produces.
Design principles
- One deep module. The whole feature is a single adapter module in
xvc-pipelinewith a two-function interface:pipeline_schema_from_kdl(&str) -> XvcPipelineSchemaandpipeline_schema_to_kdl(&XvcPipelineSchema) -> String. Everything else — DAG construction, step state machine, ECS stores,import/exportplumbing — is reused unchanged. The KDL layer knows nothing about execution; the executor knows nothing about KDL. - Information hiding. Runtime bookkeeping (content digests, metadata, entity ids) never appears in a KDL document. A KDL file contains only what a user would type.
- Define errors out of existence. Node ids default to the dependency's own
text (a file node for
data.csvis nameddata.csvunless renamed); one-off dependencies can be declared inline in a step without naming a node;whendefaults toby_dependencies; metric formats are inferred from file extensions. - Somewhat general-purpose. The language maps to
XvcPipelineSchema, not to CLI flags, so future dependency types get a node type for free once they are added toXvcDependency.
The graph model
A pipeline is a directed graph.
graph LR
images["node images<br/>(glob-items data/images/*)"] -->|preprocess| prep["data/train.bin"]
params["node params<br/>(param params.yaml::train)"] -->|train| model["models/model.pt"]
prep -->|train| model
prep -->|train| metrics["metric results/metrics.json"]
- A node declares a dependency: something whose change invalidates the steps that consume it.
- A step is an edge bundle: it consumes a set of nodes (
deps), runs acommand, and produces outputs (outs). - Outputs are the step's outgoing endpoints. When a produced file is also
consumed by another step's file/glob node, the two steps are ordered
implicitly — the runner already derives these edges from outputs
(
dependencies_to_path). Explicit step-to-step edges are written withafter, which maps to the existingstepdependency type.
Language
One KDL document defines one pipeline.
version 1
pipeline "train-model" {
workdir "."
// ── Nodes: what the pipeline observes ────────────────────────────
node "images" glob-items="data/images/*"
node "params" param="params.yaml::train"
node "top-users" sqlite-file="people.db" sqlite-query="SELECT count(*) FROM people"
// ── Edges: commands that connect nodes to outputs ────────────────
step "preprocess" command="python src/preprocess.py" {
deps "images"
outs {
file "data/train.bin"
}
}
step "train" command="python src/train.py" when="by_dependencies" {
deps "params" file="data/train.bin"
after "preprocess"
outs {
file "models/model.pt"
metric "results/metrics.json"
image "plots/loss.png"
}
}
}
version
Top-level, optional, defaults to 1. Reserved so the language can evolve
without breaking existing pipeline files: a future incompatible change bumps
this number and the parser can dispatch on it.
pipeline
Top-level, exactly one. The single argument is the pipeline name (the CLI
--pipeline-name flag overrides it, as with JSON/YAML import). Optional child
workdir "dir" sets the pipeline run directory relative to the repository
root (default: repository root).
node — dependencies
node "<id>" <type>="<spec>" declares a dependency node. <id> is the name
steps use to reference it and must be unique in the pipeline; when omitted,
the id is the <spec> text itself. Exactly one type property must be present.
The <spec> syntax of every type is identical to the corresponding
xvc pipeline step dependency CLI flag:
| Node type property | step dependency flag | Spec syntax |
|---|---|---|
file | --file | path |
glob | --glob | pattern |
glob-items | --glob-items | pattern |
param | --param | file::key (hierarchical keys ok) |
regex | --regex | file:/regex |
regex-items | --regex-items | file:/regex |
lines | --lines | file::begin-end |
line-items | --line-items | file::begin-end |
url | --url | https://... |
generic | --generic | shell command |
sqlite-file + sqlite-query | --sqlite-query | file and query as two properties |
The -items variants track individual items (files, lines) so the step
command can use ${XVC_GLOB_ITEMS}-style environment variables; the plain
variants only track a digest. This mirrors the CLI exactly.
step — commands
step "<name>" command="<shell command>" [when="..."] declares an edge.
when is one of by_dependencies (default), always, never — the same
values as step new --when / step update --when. Children:
deps— arguments are references to declared node ids; properties declare anonymous inline nodes with the same type properties asnode. Multipledepschildren are allowed and merge. Referencing an undeclared id is an error.after "<step>" ...— explicit step dependencies (CLI--step). Kept distinct fromdepsbecause it is an edge between commands, not a data node.outs— children declare outputs, matchingstep output:file "path"(--output-file),metric "path" [format="json|csv|tsv"](--output-metric, format inferred from extension when omitted), andimage "path"(--output-image).
Feature parity
Every pipeline-shaping CLI command has a KDL counterpart; read-only and execution commands operate on the imported result and need none:
| CLI | KDL |
|---|---|
pipeline new / update --workdir | pipeline node, workdir child |
step new / step update (--command, --when) | step with command, when |
step remove | delete the step, re-import with --overwrite |
step dependency (all 12 types) | node types / inline deps / after |
step output (file, metric, image) | outs children |
pipeline delete, run, list, dag, step list/show | unchanged, format-agnostic |
CLI integration
XvcSchemaSerializationFormat gains a Kdl variant recognized by the .kdl
extension. No new subcommands:
$ xvc pipeline export --file pipeline.kdl # write current pipeline as KDL
$ xvc pipeline import --file pipeline.kdl # define a new pipeline
$ xvc pipeline import --file pipeline.kdl --overwrite # modify: edit file, re-import
$ xvc pipeline run # unchanged
Export emits the canonical graph form: one named node per distinct
dependency (shared dependencies become shared nodes), after for step
dependencies, and no digest/metadata noise. export | import round-trips.