Pairing with an Agent: Interactive Development Tutorial

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.


Prerequisites


The workflow in 30 seconds

1. 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.


Step 0: Set up the example project

Create a project directory and a sample CSV:

t init  --project sales-analysis

Create 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.


Step 1: Agent generates the pipeline

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.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 = 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:

What 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.


Step 2: Agent validates with t check --schema

The agent runs structural validation:

$ t check --json pipeline.t

The pipeline has a typopd.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:

No 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.t

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

The 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.


Step 3: Agent applies fixes (or edits manually)

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-run before 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.


Step 4: Human reviews, agent builds with t run

Now 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.t

Output:

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.


Step 5: Agent diffs with t diff

After 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.t
Name          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 summary changed but you only edited clean, that’s expected (downstream dependency). If something you didn’t touch changed, investigate.


Iterating: the next edit

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.t

If clean, it builds:

$ t run pipeline.t

Then diffs:

$ t diff pipeline.t
Name          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.


Watch mode: continuous validation

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... ok

It 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.


Advanced: streaming with t run --json

For 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/null

Each 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.


Programmatic test results with t test --json

When 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 CI

Filtering 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 failed

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

  1. Agent modifies code
  2. Agent runs t test --json tests/
  3. Agent parses JSON to check status field
  4. If status is "failed", agent reads error field from failed results
  5. Agent fixes the issue and re-runs tests

The JSON output follows the same schema version as t check --json and t run --json, making it easy to integrate with existing agent tooling.

REPL-callable version

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:


Tips for effective pairing

What to tell the agent

What to watch for

Common mistakes the agent makes

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

When to step in


Quick reference

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

Exit codes for 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

Node constructors

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.


Further reading