5  Your First T Pipeline

5.1 Introduction

In the previous chapter we set up a T project: we bootstrapped T, declared dependencies in tproject.toml, and entered the project’s nix develop shell. Now it is time to actually build something.

This chapter is deliberately hands-on. We will write a pipeline, build it, break it, inspect it, fix it, and extend it. Theory comes later; Chapter 6 explains why pipelines work the way they do. Here the goal is to get your hands on the machinery and get comfortable with the core development loop.

5.2 How a T Pipeline Executes

Before writing code, it helps to understand the five stages every T pipeline goes through from declaration to result:

  1. Declare. You define nodes inside a pipeline { ... } block. Each node gets a name, a command, a runtime, and a serializer.
  2. Analyze. T scans the DAG: it resolves which node depends on which, detects cycles, and extracts identifier references from foreign-code blocks for dependency tracking.
  3. Emit. T generates one Nix derivation per node. Each derivation bundles the right runtime (R, Python, Julia, Quarto, or shell), the packages from tproject.toml, and the serializer/deserializer glue code.
  4. Build. Nix executes each derivation in a hermetic sandbox. Upstream artifacts are already materialized in the Nix store, so the deserializer reads them in and the serializer writes the node’s output, no shared memory, no runtime coupling.
  5. Read back. After build_pipeline(p) completes, use read_node(p.name) to load any node’s artifact back into T for inspection.

Key insight: A pipeline is a declarative build graph, not a script. Nodes describe what to produce, not when to compute. T handles the ordering, caching, and reproducibility: the same pipeline produces the same results on any machine.

5.3 The Simplest Possible Pipeline

Open src/pipeline.t and write:

p = pipeline {
  x = 10
  y = 20
  total = x + y
}

build_pipeline(p)

This pipeline doesn’t use any R, Python or Julia code yet, but is written in pure T. As shown in the previous chapter, T comes with many builtin functions, and even data manipulation verbs inspired by {dplyr}! We return to them in Chapter 14.

Now let’s run the pipeline. There are two ways of doing it. From the terminal (inside of the environment):

nix develop
t run src/pipeline.t

or inside an interactive T session, using t_make().

T resolves the dependency graph (total depends on x and y), builds each node, and caches the results. Access any node’s value with dot notation in the REPL:

p.x      -- 10
p.y      -- 20
p.total  -- 30

The pipeline itself prints as:

Pipeline(3 nodes: [x, y, total])

Note that the order of node declarations does not matter. T infers the execution order from the data dependencies. You could write total before x and y and the result would be identical.

5.4 The Core Development Loop

A productive way to build pipelines is incrementally: start small, build, inspect, extend:

t check src/pipeline.t   # validate cheaply, catches errors in milliseconds
t run src/pipeline.t     # build when clean

Inside the REPL (t or t repl) you can inspect results immediately after a build:

read_node(p.total) -- reads the serialized artifact back from the Nix store
show_plot(p)       -- visualise the DAG

Add a node and rebuild:

p = pipeline {
  x     = 10
  y     = 20
  total = x + y
  label = str_sprintf("Sum is %d", total)
}
build_pipeline(p)

Because x, y, and total were already built and cached, Nix skips them and only computes the new label node. Run build_pipeline(p, dry_run = true) to preview which nodes will be rebuilt versus served from cache before committing to a full build.

This check → build → inspect → extend loop is the core iterative development cycle in T. Get comfortable with it before moving on. It is also possible to interactively inspect R, Python or Julia nodes. More on this later.

Once the pipeline is built, you still need to get to the build artifacts. For this, use pipeline_copy(). This copies all computed node artifacts from the Nix store to a local directory for easy access. By default, it copies all nodes from the latest build to the “pipeline-output/” folder. You can specify a custom folder using the target_dir argument: pipeline_copy(target_dir = "outputs")

5.5 Node Types: Calling R, Python, and Julia

The power of T is that nodes can run in any runtime. The node wrapper functions make this explicit:

Function Runtime Use case
node() T native Data manipulation, logic, native T operations (optional)
rn() R Statistical modelling, tidyverse, Bioconductor
pyn() Python Machine learning, scikit-learn, PyTorch
jln() Julia High-performance numerics, Flux.jl
shn() Shell File processing, calling external CLIs

Foreign-language code lives inside <{...}> raw code blocks, which pass code verbatim to the target runtime without T parsing it:

p = pipeline {
  -- Load data in T
  data = read_csv("data/mtcars.csv", separator = "|")

  -- Summarise in R using dplyr
  summary_r = rn(
    command   = <{
      data |>
      dplyr::group_by(cyl) |>
      dplyr::summarize(avg_mpg = mean(mpg))
    }>,
    deserializer = ^ipc,
    serializer   = ^ipc
  )

  -- Count groups in Python using pandas
  counts_py = pyn(
    command   = <{ summary_r.groupby("cyl").size() }>,
    deserializer = ^ipc,
    serializer   = ^json
  )
}

build_pipeline(p)

T reads the R node’s output (summary_r, serialized as ^ipc Arrow IPC) and makes it available inside the Python node’s sandbox under the same name. Serialization and deserialization happen automatically, you never write glue code to convert between R data frames and Python DataFrames.

5.6 Serializers

Every node in a T pipeline produces an artifact. The serializer argument controls the format of that artifact, and the deserializer argument controls how an upstream artifact is loaded into the current node’s sandbox. Both use the ^ prefix:

Serializer T Type Best for
^csv DataFrame Portable tabular data between nodes
^ipc DataFrame Fast Arrow IPC hand-off between pipeline nodes
^parquet DataFrame Durable storage, archival, external analytics
^json Dict / List Structured data interchange
^pmml Model R/Python models, native T evaluation
^onnx Model Python ML models, portable evaluation
^text String Plain text and shell output

Rule of thumb: use ^ipc to pass DataFrames between nodes while a pipeline runs (fastest, uncompressed), and ^parquet for anything you persist, ship, or store (4–10× smaller, compatible with DuckDB, Spark, pandas, etc.). Both work symmetrically across T, R, Python, and Julia.

When no serializer is specified, T picks a sensible default. When you are passing a DataFrame from an R node to a Python node, set serializer = ^ipc on the R node and deserializer = ^ipc on the Python node.

5.7 A Complete Worked Example

Let’s build a small but complete polyglot pipeline: load data in T, fit a logistic regression in R, and read the results back.

First, make sure tproject.toml has the R packages we need:

[r-dependencies]
packages = ["dplyr"]

Then run t update && exit && nix develop to sync the environment.

Now write the pipeline:

-- src/pipeline.t
p = pipeline {
  -- Node 1: load data in T
  data = node(
    command    = to_dataframe([
      x = [1.0, 2.0, 3.0, 4.0, 5.0],
      y = [0L,  0L,  1L,  1L,  1L]
    ]),
    serializer = ^ipc
  )

  -- Node 2: fit a logistic regression in R
  model = rn(
    command = <{
      data$y <- as.factor(data$y)
      glm(y ~ x, data = data, family = binomial(link = "logit"))
    }>,
    deserializer = ^ipc,
    serializer   = ^pmml
  )
}

build_pipeline(p, verbose = 1)

-- Read results back into T and inspect
raw = read_node(p.data)
fitted = read_node(p.model)

print("Data shape:")
print(str_sprintf("%d rows × %d cols", nrow(raw), length(colnames(raw))))

print("Model coefficients:")
print(fitted.coefficients)

Run it:

t run src/pipeline.t

You should see the build log, then the data shape and model coefficients printed. Try changing the data values and re-running; only the affected nodes rebuild.

5.8 Inspecting the Pipeline

After a build, several tools let you look inside the pipeline from the REPL:

-- See the full DAG as a table
pipeline_to_frame(p)

-- See which node depends on which
pipeline_deps(p)

-- Visualise the dependency graph
show_plot(p)

-- Deep-dive into a specific node's metadata
inspect_node(p.model)
-- Returns: runtime, serializer, path in Nix store, dependencies, warnings

-- Read the raw Nix build log for a node
read_log("model")

explain() gives a quick summary of any node:

explain(p.model)
-- {
--   `kind`: "computed_node",
--   `name`: "model",
--   `runtime`: "R",
--   `path`: "/nix/store/...-model/artifact",
--   `serializer`: "pmml",
--   `class`: "glm",
--   `dependencies`: ["data"]
-- }

The path field is the escape hatch: the absolute path to the node’s output in the Nix store. You can pass this to any external tool, or use the companion helper packages for R or Python to read T artifacts outside of T.

explain() reaches further than nodes: it describes any value, including DataFrames, errors, formulas, and whole pipelines. Chapter 15 takes it apart, alongside the intent blocks that record why a piece of code exists.

5.9 Comparing Builds with t diff

After a build you can compare it against any previous build to see exactly what changed:

t diff src/pipeline.t

This compares the most recent build against the one before it:

Name          Status    Class_a  Class_b
data          Unchanged T        T
model         Changed   T        T

The Status column tells you the blast radius of your last edit. Changed means the node’s content-addressed hash is different: the artifact actually changed, not just the code. Added means a new node. Removed means a node was deleted from the pipeline definition.

This is more reliable than diffing source code: T diffs outputs, not inputs. If you refactored a node’s code but the result is bit-for-bit identical (e.g. you renamed an internal variable), t diff shows Unchanged. If a small change cascaded through several downstream nodes, you see the full chain marked Changed.

5.10 What t check Catches

Before building, t check validates the pipeline structure without triggering any Nix builds. Validation runs in three tiers, each more expensive than the last:

Tier Flag What it checks Cost
1 t check Parse, DAG structure, node references, serializer coherence milliseconds
2 t check --schema Column names and types propagated across nodes milliseconds
3 t check --env Nix environment can be built for each node seconds

Start with tier 1 and only escalate when you want deeper validation. Tier 2 is particularly valuable: it catches column-name typos in downstream nodes before you pay for a full Nix build:

t check --schema src/pipeline.t

Pass --json at any tier for structured output that agents and CI scripts can consume:

t check --json src/pipeline.t
{
  "error_class": "schema_mismatch",
  "node": { "id": "model", "lang": "R" },
  "message": "Column 'xval' not found. Did you mean 'x'?",
  "caused_by": ["data"],
  "suggested_fix": { "kind": "rename_column", "old_name": "xval", "new_name": "x" }
}

Notice the suggested_fix field: when T can infer the correct action, it says so explicitly. t fix applies these mechanical fixes automatically:

t fix --dry-run src/pipeline.t   # preview what would change
t fix src/pipeline.t             # apply

Always preview before applying: a rename fix changes column references throughout the file. The --dry-run output shows exactly which lines are affected.

For continuous validation while actively editing, run t check in watch mode in a separate terminal:

t check --watch --schema src/pipeline.t

It re-runs automatically on every file save. Exit codes are consistent and scriptable:

Exit code Meaning
0 Clean, no errors
1 Structural / DAG problem
2 Column / type mismatch
3 Nix environment problem

Make it a habit: t check first, t run only when clean.

5.11 Interactive Development with the T REPL in Positron

Non-interactive batch execution via shell commands like t run src/pipeline.t is perfect for automated builds, reproducible pipelines, and CI/CD runners. But humans rarely write data science workflows as single monolithic batch runs. Data analysis is inherently iterative: we need to explore intermediate outputs, check data shapes, inspect model coefficients, test function logic, and troubleshoot errors in real time. Thankfully, T provides a REPL (Read-Eval-Print Loop).

When working in Positron (or VS Code with the T extension), you can open an interactive t session directly in your editor terminal. Running t (or t repl) inside Positron gives you a live, interactive environment where you can execute pipeline definitions, evaluate build_pipeline(p) on the fly, and inspect nodes immediately. For this, you do need to configure your editor. Please follow the instructions over at https://github.com/b-rodrigues/tlang/blob/main/docs/editors.md to correctly configure your editors.

5.11.1 Inspecting Live Nodes: read_node()

Once a pipeline is built in your session, you can inspect any node’s computed result using read_node(). Suppose that we are working on the pipeline from before. To run it from the REPL, use t_make():

t_make()

You can now read the contents of individual nodes:

read_node(p.data)

Note that read_node() strictly expects a ComputedNode reference (such as p.data or p.stats), not a raw string name.

5.11.2 Inspecting Historical Builds: read_past_node()

What if you want to inspect a node’s output from a previous build, or examine artifacts when the pipeline object p is no longer in your active REPL memory?

T provides read_past_node() for temporal introspection:

read_past_node(p.data, which_log = "2026-08-14")

Unlike read_node(), which looks up live in-scope nodes from the active pipeline object, read_past_node() captures the node identifier non-evaluatively and queries T’s historical build logs in _pipeline/logs/. The mandatory which_log parameter specifies a timestamp or log identifier pattern. This allows you to inspect past run outputs, audit how dataset values evolved over time, or inspect artifacts from previous sessions without re-running the pipeline.

5.11.3 High-Fidelity Representation of Foreign Objects

One of the greatest benefits of the T REPL is how it displays results from foreign runtimes. Whether a node was computed in pure T, R (rn), Python (pyn), or Julia (jln), read_node() automatically deserializes the underlying artifact (Parquet, Arrow IPC, JSON, PMML, etc.) and converts it into a native, high-fidelity T representation.

When you call read_node() on a foreign node from the T REPL, T formats the output cleanly:

  • DataFrames (from R tibble/data.frame, Python pandas/polars, or Julia DataFrame) print with column names, data types, and row/column counts.
  • Models (such as an R glm serialized as PMML or a Python scikit-learn model) display structured model metadata and tidy coefficient tables via fitted.coefficients.
  • Structured collections (Lists and Dicts) display as clean, readable trees without low-level string formatting artifacts.

You get rich, instant feedback inside your interactive Positron session without writing custom glue code or manually serializing objects to temporary files. If the node produces an artifact that T doesn’t know how to deserialize, it is still possible to start a temporary R or Python (or Julia) shell to inspect that object, using debug_node(). Keep reading.

5.11.4 Stepping into Guest Runtimes: debug_node() (A Debugging Appetizer)

Sometimes inspecting an output from the T REPL isn’t enough, especially when a foreign script throws an unexpected runtime error or requires step-by-step code execution.

T provides debug_node() to drop you directly from the T REPL into an interactive guest subshell for that specific language:

debug_node(p.stats)

Calling debug_node() automatically:

  1. Resolves all upstream dependency artifacts for p.stats from the Nix store.
  2. Sets up environment variables and imports context.
  3. Spawns an interactive REPL for the target language: a Python subshell with prompt py>, an R subshell with prompt r>, or a Julia subshell with prompt jl>.

Inside the guest subshell, the tlang companion library is available, allowing you to load upstream node data live:

py> import tlang
py> data = tlang.read_node("data")
py> print(data.head())

To preserve workspace reproducibility during debugging, imperative package manager commands (such as install.packages(), pip install, or Pkg.add()) are dynamically intercepted and blocked inside debug_node() subshells. If your code requires an additional package, simply declare it in tproject.toml and re-enter nix develop.

Pressing Ctrl+D (or typing exit()) exits the guest subshell and returns control seamlessly back to your parent T REPL session.

(We will explore interactive subshells, failure logs, stack trace analysis, and advanced troubleshooting techniques in detail in an upcoming chapter dedicated to debugging).

5.12 Pointing Nodes at External Scripts

You do not have to inline all your code in <{ ... }> blocks. The script argument lets a node point to an external file:

p = pipeline {
  data  = node(command = read_csv("data/sales.csv"), serializer = ^ipc)
  model = rn(script = "src/train.R", deserializer = ^ipc, serializer = ^pmml)
  report = node(script = "src/report.qmd", runtime = Quarto)
}
build_pipeline(p)

The runtime is inferred from the file extension (.R → R, .py → Python, .jl → Julia, .sh → Shell, .qmd → Quarto). T reads the file to extract identifier references, so the dependency graph is still built correctly. We will return to this in Chapter 6, where we discuss why external script files make your analysis code more portable and testable.

5.13 Pairing with an AI Agent

T is designed for AI-assisted development. The AGENTS.md and T-LANGUAGE-REFERENCE.md files that t init generates give any LLM the context it needs to write correct T pipelines. But the workflow matters as much as the context.

The core principle: t check is cheap (milliseconds), t run is expensive (minutes, the first time Nix builds an environment). An AI agent should iterate on t check until the pipeline is structurally clean, then trigger t run once.

The recommended loop:

1. Agent generates pipeline
2. Agent runs: t check --json src/pipeline.t
3. Agent reads diagnostics, fixes errors
4. Repeat steps 2-3 until exit code 0
5. Human reviews the pipeline
6. Agent runs: t run src/pipeline.t
7. Agent runs: t diff src/pipeline.t  ← confirms what actually changed

5.13.1 A worked example: building a pipeline with an agent

Let’s watch the loop in action on a small project. You have a CSV of sales and you want to filter out non-positive amounts and summarise total sales per region.

Step 0: set up the project. Create the project and a sample data file:

t init --project sales-analysis

Create data/sales.csv:

id,region,amount,date,product
1,North,250.00,2026-01-15,Widget
2,South,-12.50,2026-01-16,Gadget
3,North,180.75,2026-01-17,Widget
4,East,420.00,2026-01-18,Gadget
5,West,95.00,2026-01-19,Widget
6,North,310.25,2026-01-20,Gadget
7,South,0.00,2026-01-21,Widget
8,East,175.50,2026-01-22,Gadget

Add the packages you need to tproject.toml and run t update yourself before you start, telling the agent exactly what you need (and to add nothing else) reduces friction. One thing worth checking up front: make sure the agent actually sees the project’s SKILL.md. A quick “What skills are available to you in this project?” confirms it has the T context loaded before you ask it to write anything.

Step 1: the agent generates the pipeline. Ask for what you want:

“Create a T pipeline that reads data/sales.csv with a T node, then uses Python nodes to filter out zero and negative amounts, convert the date column, group by region, and summarize total sales per region.”

The agent writes pipeline.t:

p = pipeline {
  raw = node(
    command = read_csv("data/sales.csv"),
    serializer = ^csv
  )

  clean = pyn(
    command = <{
import pandas as pd
df = raw.copy()
df = df[df["amount"] > 0]
df["date"] = pd.to_datetme(df["date"])
df
    }>,
    deserializer = ^csv,
    serializer = ^csv
  )

  summary_node = pyn(
    command = <{
import pandas as pd
result = clean.groupby("region")["amunt"].sum().reset_index()
result.columns = ["region", "total"]
result
    }>,
    deserializer = ^csv,
    serializer = ^csv
  )
}

build_pipeline(p)

Notice the structure: raw is a native T node (T reads the CSV itself), while clean and summary_node are Python nodes. Each Python node receives its upstream data as a pandas DataFrame via deserializer = ^csv, and the bare names raw and clean inside the <{ ... }> blocks are auto-detected as dependencies. And remember the hermetic-sandbox rule from above: any data that must be downloaded belongs in fetchurl()/prefetch(), not inside a node.

Note

What the human reviews. Skim the pipeline before anything runs. Does it do what you asked? Are the node names clear and the data flow obvious? If the agent misunderstood the goal, correct it now: regenerating two nodes is far cheaper than regenerating ten.

Step 2: the agent validates with t check --json. This is where the cheap/expensive split pays off. The first check catches a typo in the Python code:

{
  "status": "error",
  "phase": "parse",
  "tier": 1,
  "diagnostics": [
    {
      "id": "T0101",
      "error_class": "name_error",
      "severity": "error",
      "node": { "id": "clean", "lang": "python", "span": { "start": [12, 14], "end": [12, 30] } },
      "message": "Name 'pd.to_datetme' is not defined in node 'clean'"
    }
  ]
}

The agent reads error_class: "name_error", locates node clean, and fixes pd.to_datetme to pd.to_datetime. Re-checking now surfaces a different, deeper error, one that only appears once the parse phase is clean:

{
  "status": "error",
  "phase": "schema",
  "tier": 2,
  "diagnostics": [
    {
      "id": "T0142",
      "error_class": "schema_mismatch",
      "severity": "error",
      "node": { "id": "summary_node", "lang": "python" },
      "message": "Column 'amunt' not found. Did you mean 'amount'?",
      "caused_by": ["clean"],
      "suggested_fix": { "kind": "rename_column", "old_name": "amunt", "new_name": "amount" }
    }
  ]
}

caused_by: ["clean"] tells the agent the bad column name is inherited from upstream, and the suggested_fix names the exact rename. The agent corrects the reference and re-checks:

{ "status": "ok", "phase": "wire", "tier": 2, "diagnostics": [] }

Clean. The pipeline is structurally sound.

Note

What the human reviews. Ask the agent to show what it changed and why. A rename touches column references throughout the file: make sure the agent understands the root cause of the mismatch, not just how to silence it.

Step 3: apply mechanical fixes. In this case the agent edited the code directly, so no t fix was needed. When a suggested_fix is applicable, preview before applying, as covered above:

t fix --dry-run pipeline.t   # preview
t fix pipeline.t             # apply

Step 4: the human approves, the agent builds. Only now is t run triggered: the step that downloads dependencies and builds each node’s Nix sandbox:

$ t run pipeline.t
Node 'raw' building...
Node 'raw' completed (0.3s)
Node 'clean' building... (Python environment)
Node 'clean' completed (4.2s)
Node 'summary_node' building... (Python environment)
Node 'summary_node' completed (2.1s)
Pipeline complete. 3/3 nodes succeeded.
Note

What the human reviews. Confirm every node succeeded. If one failed, the message names the node and the reason: the agent should parse it and explain what went wrong rather than retry blindly.

Step 5: the agent diffs with t diff. After a build, t diff shows the blast radius of the last edit:

$ t diff pipeline.t
Name          Status    Class_a  Class_b
raw           Unchanged T        T
clean         Unchanged T        T
summary_node  Unchanged T        T

On the first build there is nothing to compare, so everything is Unchanged. After an edit you would see the affected nodes (and their downstream dependents) marked Changed.

Iterating. Say you now want a per-product breakdown. You tell the agent:

“Add a fourth node that groups by product and sums the amount, same pattern as the region summary.”

The agent adds a by_product Python node, re-runs t check --json, and only when clean runs t run. The diff then shows the new node:

Name          Status    Class_a  Class_b
raw           Unchanged T        T
clean         Unchanged T        T
summary_node  Unchanged T        T
by_product    Added     -        T

The whole loop stays fast because t check catches structural errors in milliseconds; the agent only pays for t run once the pipeline is known to be well-formed.

5.13.2 Streaming build events with t run --json

For long pipelines, t run can stream NDJSON events so an agent can react to the first failure instead of waiting for the whole build to finish:

$ t run --json pipeline.t 2>/dev/null

Each line is a JSON object. The run begins with a run_started event listing the nodes and their dependencies:

{"seq":1,"event":"run_started","file":"pipeline.t","nodes":[{"id":"raw","lang":"t"},{"id":"clean","lang":"python","depends_on":["raw"]},{"id":"summary_node","lang":"python","depends_on":["clean"]}]}

If a node fails, a node_failed event carries a log_tail, the last 200 lines of the build log, enough to see the real traceback without flooding the output:

{"seq":2,"event":"node_failed","node":{"id":"clean","lang":"python"},"error_class":"nix_error","message":"Nix build failed for node 'clean'","log_tail":"pandas.errors.ParserError: Error tokenizing data..."}

When the run finishes, run_finished reports the overall status and, crucially, root_causes, the node(s) that actually caused the failure, i.e. the one the agent should fix first:

{"seq":4,"event":"run_finished","file":"pipeline.t","status":"failed","total_nodes":3,"failed":1,"skipped":1,"root_causes":["clean"]}

When working with an agent, a few things to keep in mind:

Tell the agent to check before running. Most agents will do this if you set the expectation explicitly: “Always run t check --json and confirm it exits with code 0 before running t run”. Without this instruction, agents sometimes skip straight to t run and waste several minutes on a build that fails immediately.

Ask for --dry-run before t fix. When the agent proposes applying a suggested_fix, ask it to preview first: t fix --dry-run src/pipeline.t. A rename fix modifies column references throughout the file: mechanical and correct, but you should confirm it matches your intent before applying.

Correct misunderstandings early. If the agent generates a pipeline that misunderstands the goal, fix it before it generates ten more nodes. Regenerating two nodes is cheap; regenerating ten is not.

The diff is your audit trail. After every t run, ask the agent to show you t diff. If only the nodes you intended to change show Changed, everything is working as expected. If something you did not touch shows Changed, investigate before accepting the build.

Data that needs to be downloaded belongs in fetchurl, not in a node. Build sandboxes are hermetic: a Python node cannot call requests.get() to pull data from the internet. Use fetchurl(url, sha256 = prefetch(url)) in T to download assets before pipeline execution. The agent must understand this constraint; mention it when you set up the project.

The most common ways a generated pipeline goes wrong, and the t check tier that catches each one, are worth memorising:

Mistake Caught by
Typo in a Python/pandas function name t check: name_error
Wrong column name in a downstream node t check --schema: schema_mismatch
Missing serializer on a node t check: structural_error
Wrong deserializer format t check: structural_error
TipQuick reference: agent-facing CLI
Command Cost What it does
t check <file> ms Tier 1: structure + DAG
t check --schema <file> ms + tier 2: column/type propagation
t check --env <file> seconds + tier 3: Nix env validation
t check --watch --schema <file> ms/save Continuous validation on file save
t check --json <file> same Structured JSON output
t fix --dry-run <file> ms Preview mechanical fixes
t fix <file> ms Apply mechanical fixes
t run <file> minutes Execute pipeline (Nix build)
t run --json <file> minutes Streaming NDJSON events
t diff <file> ms Compare last two builds

5.14 Summary

The core workflow you will use throughout the rest of this book:

  1. t check --schema src/pipeline.t: validate cheaply, tier 1 + 2, in milliseconds
  2. t fix --dry-run / t fix: apply mechanical fixes suggested by t check
  3. t run src/pipeline.t: build when clean; Nix caches unchanged nodes
  4. t diff src/pipeline.t: confirm what actually changed after a build
  5. read_node(p.name) in the REPL: inspect any node’s output
  6. Extend one node at a time: rebuild only what changed

Key things to remember:

  • Node declaration order does not matter; T infers execution order from data dependencies
  • rn(), pyn(), jln(), shn() run code in their respective runtimes; node() runs native T
  • <{ ... }> passes code verbatim to the target runtime without T parsing it
  • Serializers (^ipc, ^parquet, ^pmml, …) control what the node produces and what downstream nodes receive
  • Use ^ipc for fast in-pipeline hand-offs; use ^parquet for durable storage
  • Never call install.packages() or pip install inside a node: declare everything in tproject.toml
  • Data downloads go in fetchurl(), not inside node commands: build sandboxes are hermetic and have no network access
  • When pairing with an AI agent: check first, run second, diff always

In the next chapter, we step back and ask why pipelines are designed this way, connecting T’s node model to the ideas of functional programming: pure functions, composition, and immutability.