This tutorial walks you through building a T pipeline interactively with an AI agent. You’ll see the full workflow — from blank file to verified pipeline — with real terminal output at every step.
Time: ~15 minutes for the first run-through.
t --version works)docs/nix-installation.md)t commands1. Agent generates the pipeline
2. Agent validates with t check --schema ← catches errors in milliseconds
3. Agent fixes errors (t fix or manual)
4. Human reviews, agent builds with t run ← only now does Nix build
5. Agent diffs with t diff ← confirms what changed
The key insight: t check is cheap,
t run is expensive. The agent iterates on
t check until the pipeline is structurally sound,
then triggers the Nix build. This keeps the feedback loop fast
— seconds, not minutes.
Create a project directory and a sample CSV:
t init --project sales-analysisCreate a sample data file at 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 required packages in tproject.toml and rebuild
the environment using t update. Agents are capable of doing
it, but doing it yourself reduces friction. If you know exactly what you
need, clearly instruct your agent to not add packages to
tproject.toml.
Verify that your agent knows about the included SKILL.md
file:
“What skills are available to you in this project?”
Then tell your agent what you want:
“Create a T pipeline that reads
data/sales.csvwith 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 = 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 pipeline structure:
raw is a T node that reads the CSV (T
handles file I/O natively)clean is a Python node
(pyn) that filters and transforms the datasummary is a Python node that groups
and aggregatesdeserializer = ^csvraw, clean)
inside <{ ... }> blocks are auto-detected as
dependencies — T deserializes the upstream artifact and injects it as a
variableWhat the human reviews: Skim the pipeline. Does it do what you asked? Are the node names clear? Is the data flow obvious? If the agent misunderstood the goal, correct it now — it’s cheap to regenerate.
Pay close attention to the serializers used, and if you need to
download assets from the internet, make sure the agent correctly plans
to use fetchurl() and prefetch(). Downloading
from a node will not work, as the build sandboxes are hermetic.
t check --schemaThe agent runs structural validation:
$ t check --json pipeline.tThe pipeline has a typo — pd.to_datetme
instead of pd.to_datetime:
{
"schema_version": "1",
"status": "error",
"phase": "parse",
"tier": 1,
"diagnostics": [
{
"id": "T0101",
"error_class": "name_error",
"severity": "error",
"phase": "parse",
"node": {
"id": "clean",
"lang": "python",
"file": "pipeline.t",
"span": { "start": [12, 14], "end": [12, 30] }
},
"message": "Name 'pd.to_datetme' is not defined in node 'clean'",
"expected": null,
"actual": null,
"caused_by": [],
"suggested_fix": null
}
]
}The agent sees:
error_class: "name_error" — an
undefined namenode.id: "clean",
node.lang: "python" — the error is in the Python
nodespan: [12, 14] — line 12, column 14 in
the pipeline fileNo suggested_fix for typos — the agent has to fix this
itself. It corrects pd.to_datetme to
pd.to_datetime and re-checks:
$ t check --json pipeline.tt check --schema runs in stages: the parse phase
(tier 1) must be clean before the schema phase (tier
2) runs. Because the command had a name error, column analysis
never got a chance to run — so this next problem only surfaces now that
the parse phase passes:
Now a different error surfaces:
{
"schema_version": "1",
"status": "error",
"phase": "schema",
"tier": 2,
"diagnostics": [
{
"id": "T0142",
"error_class": "schema_mismatch",
"severity": "error",
"phase": "schema",
"node": {
"id": "summary",
"lang": "python",
"file": "pipeline.t",
"span": { "start": [22, 35], "end": [22, 41] }
},
"message": "Column 'amunt' not found. Did you mean 'amount'?",
"expected": null,
"actual": null,
"caused_by": ["clean"],
"suggested_fix": {
"kind": "rename_column",
"old_name": "amunt",
"new_name": "amount",
"edit_distance": 1,
"is_unique": true,
"target_node": "summary"
}
}
]
}The agent sees:
error_class: "schema_mismatch" —
column name doesn’t match upstream schemacaused_by: ["clean"] — the
summary node reads from clean’s outputsuggested_fix — rename
amunt to amount in the summary nodeThe agent fixes the column reference:
result = clean.groupby("region")["amount"].sum().reset_index()Re-check:
$ t check --json pipeline.t{
"schema_version": "1",
"status": "ok",
"phase": "wire",
"tier": 2,
"diagnostics": []
}Clean. The pipeline is structurally sound. Time to build.
What the human reviews: The agent should show you what it’s changing and why. A rename fix changes column references throughout the file — make sure the agent understands why the name mismatch happened, not just how to suppress it.
In this case the agent edited the Python code directly (fixing the
column typo). No t fix needed — the
suggested_fix was a rename_column, but the
agent chose to fix the reference directly instead.
If t fix had been applicable, the agent would preview
first:
$ t fix --dry-run pipeline.t
dry-run: would apply 1 fix to pipeline.t:
[Rename_column] line 22: rename column 'amunt' to 'amount' in node 'summary'Then apply:
$ t fix pipeline.t
Applied 1 fix(es), skipped 0.
Run 't check pipeline.t' to verify.What the human reviews: Always ask the agent to run
t fix --dry-runbefore applying. A rename fix modifies column references in the file — mechanical, but you should confirm it matches your intent. Sometimes fixing the root cause (as the agent did here) is better than applying the suggested fix.
t runNow that t check --schema passes, the pipeline is safe
to build. This is the step that triggers Nix — it will download Python
dependencies, build each node’s sandbox, and execute the pipeline.
$ t run pipeline.tOutput:
Node 'raw' building...
Node 'raw' completed (0.3s)
Node 'clean' building... (Python environment)
Node 'clean' completed (4.2s)
Node 'summary' building... (Python environment)
Node 'summary' completed (2.1s)
Pipeline complete. 3/3 nodes succeeded.
The pipeline ran successfully. Each node built its Nix environment and executed. The Python nodes took longer because Nix set up a Python environment with pandas.
What the human reviews: Check the output — did all nodes succeed? If a node failed, the error message tells you which node and why. The agent should parse this and explain what went wrong.
t diffAfter the first successful build, the agent can check what changed.
If you’ve run the pipeline before, t diff compares the two
builds:
$ t diff pipeline.tName Status Class_a Class_b
raw Unchanged T T
clean Unchanged T T
summary Unchanged T T
All nodes unchanged — this is the first build, so there’s nothing to compare against. After an edit, you’d see:
Name Status Class_a Class_b
raw Unchanged T T
clean Changed T T
summary Changed T T
This tells you the blast radius: your edit to clean
cascaded to summary.
For programmatic access, use diff_summary() in the
REPL:
p = build_pipeline(pipeline { ... })
d = diff_summary(p)
# Returns a DataFrame with columns: name, status, hash_a, hash_b
What the human reviews: The diff tells you whether the agent’s edit had the intended effect. If
summarychanged but you only editedclean, that’s expected (downstream dependency). If something you didn’t touch changed, investigate.
Say you want to add a 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 edits pipeline.t, adding a
by_product Python node. It immediately runs:
$ t check --json pipeline.tIf clean, it builds:
$ t run pipeline.tThen diffs:
$ t diff pipeline.tName Status Class_a Class_b
raw Unchanged T T
clean Unchanged T T
summary Unchanged T T
by_product Added - T
The by_product node is new — t diff shows
it as Added.
This loop is fast because t check catches structural
errors in milliseconds. The agent only pays for t run when
the pipeline is known to be well-formed.
While the agent is actively editing, you can run t check
in watch mode in a separate terminal:
$ t check --watch --schema pipeline.t
Checking pipeline.t... okIt re-runs automatically every time the file is saved. No need to manually re-check after each agent edit — the output updates in place.
Press Ctrl+C to stop. The exit code reflects the last check’s result, so you can use it in scripts.
t run --jsonFor long-running pipelines (many nodes, expensive builds),
t run --json streams NDJSON events so the agent can react
to the first failure without waiting:
$ t run --json pipeline.t 2>/dev/nullEach line is a JSON object. First, the run starts:
{"schema_version":"1.0","seq":1,"ts":"2026-07-14T12:00:00.000Z","event":"run_started","file":"pipeline.t","nodes":[{"id":"raw","lang":"t"},{"id":"clean","lang":"python","depends_on":["raw"]},{"id":"summary","lang":"python","depends_on":["clean"]}]}If a node fails:
{"schema_version":"1.0","seq":2,"ts":"2026-07-14T12:00:05.123Z","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..."}The log_tail contains the last 200 lines of the build
log — enough to see the actual Python traceback without flooding the
output.
When everything finishes:
{"schema_version":"1.0","seq":4,"ts":"2026-07-14T12:00:10.456Z","event":"run_finished","file":"pipeline.t","status":"failed","total_nodes":3,"failed":1,"skipped":1,"root_causes":["clean"]}The root_causes field tells the agent which node is the
actual source of the failure — the one it should fix first.
t test --jsonWhen an agent needs to verify that its changes don’t break existing
tests, it can use t test --json to get structured test
results. This is essential for agent workflows where test results must
be consumed programmatically.
$ t test --json tests/
$ t test --format junit tests/ # JUnit XML for CIFiltering tests:
$ t test --only "stats" # run only tests matching "stats"
$ t test --not "slow" # skip tests matching "slow"
$ t test --failfast # stop on first failure
$ t test --list # list tests without running
$ t test --timeout 30 # mark slow tests as failedExcluding tests with .tignore:
Create tests/.tignore to automatically exclude test
files:
# tests/.tignore
slow_integration.t
*_benchmark.t
legacy/
The output is a JSON object with the test suite summary:
{
"schema_version": "1",
"status": "passed",
"total": 15,
"passed": 14,
"failed": 1,
"duration_ms": 2340,
"results": [
{
"file": "tests/test_arithmetic.t",
"status": "passed",
"duration_ms": 120,
"error": null
},
{
"file": "tests/test_strings.t",
"status": "failed",
"duration_ms": 85,
"error": "Assertion failed at line 42: expected \"hello\" but got \"world\""
}
]
}Agent workflow for test-driven iteration:
t test --json tests/status fieldstatus is "failed", agent reads
error field from failed resultsThe JSON output follows the same schema version as
t check --json and t run --json, making it
easy to integrate with existing agent tooling.
For agents working in the REPL, t_test() returns a
DataFrame with the same results:
results = t_test()
-- DataFrame with columns: file, status, duration_ms, error
-- Filter to show only failed tests
failed = results |> filter($status == "failed")
nrow(failed) -- 0 if all tests passed
The DataFrame columns are:
file: Path to the test filestatus: “passed” or “failed”duration_ms: Duration in millisecondserror: Error message (NA for passed tests)t check before
t run. The agent should always validate
structurally before building. Most agents will do this if you set the
expectation.t check
automatically. Remind it: “Run t check --json
before building.”t check --json and ignores the diagnostics, tell it to
parse the diagnostics array.t fix without
previewing. Always ask for --dry-run first.--watch mode.
Remind it to run t check --watch --schema in a separate
terminal during active editing.| Mistake | How to catch it |
|---|---|
| Typo in Python/pandas function name | t check catches it as name_error |
| Wrong column name in downstream node | t check --schema catches it as
schema_mismatch |
| Missing serializer on a node | t check catches it as
structural_error |
| Wrong deserializer format | t check catches it as
structural_error |
t fix: Always review what
changed. The fix is mechanical but may not match your intent.t run: The Nix build takes
time. Make sure the pipeline is what you want before triggering it.t diff: Check that only the
nodes you intended to change actually changed.| Command | What it does | Cost |
|---|---|---|
t check <file> |
Tier 1: parse, structure, DAG | ~ms |
t check --schema <file> |
+ tier 2: column/type propagation | ~ms |
t check --env <file> |
+ tier 3: Nix environment validation | ~seconds |
t check --watch --schema <file> |
Continuous validation on file save | ~ms per save |
t check --json <file> |
Structured JSON output (any tier) | same as tier |
t fix --dry-run <file> |
Preview mechanical fixes | ~ms |
t fix <file> |
Apply mechanical fixes | ~ms |
t run <file> |
Execute pipeline (Nix build) | minutes |
t run --json <file> |
Execute with streaming NDJSON events | minutes |
t diff <file> |
Compare last two builds | ~ms |
t_diff(file, json=true) |
REPL: structured diff as DataFrame | ~ms |
t check| Code | Meaning |
|---|---|
| 0 | Clean — no errors |
| 1 | Wire error — structural/DAG problem |
| 2 | Schema error — column/type mismatch |
| 3 | Env error — Nix environment problem |
| Constructor | Runtime | Syntax for code |
|---|---|---|
node() |
T | command = expr (no wrapping needed) |
pyn() |
Python | command = <{ ... }> (raw code block) |
rn() |
R | command = <{ ... }> (raw code block) |
jln() |
Julia | command = <{ ... }> (raw code block) |
shn() |
shell | command = <{ ... }> (raw code block) |
qn() |
Quarto | script = "file.qmd" |
All node constructors also accept serializer,
deserializer, env_vars, and
deps.
t check,
t run, t diff, t fix CLI
docs