How T enables structured, auditable collaboration between humans and Large Language Models for data analysis.
Current LLM-assisted coding suffers from:
Result: LLM-generated code is useful for prototyping but hard to maintain, audit, or regenerate.
T provides a structured way to onboard LLMs to new projects via the
t init command. When a project is initialized, T generates
two essential files that should be provided to the AI agent at the start
of any conversation:
AGENTS.md)This file tells the LLM exactly how the current project is structured and what the coding conventions are (e.g., “Nix is mandatory”, “Use Arrow for data transfer”). It serves as the project’s “rules of engagement” for AI assistants.
T-LANGUAGE-REFERENCE.md)To handle different LLM context windows and project needs, T allows you to select a “Context Level” during initialization:
| Level | Description | Use Case |
|---|---|---|
| small | Core syntax and top 20 functions | Simple scripts, low-context models |
| medium | Exhaustive standard library index (Default) | General analysis and pipeline development |
| full | Comprehensive manual with detailed examples | Complex logic and package development |
| huge | Concatenated documentation of the entire ecosystem | Deep debugging and system-level tasks |
By providing these files, you ensure the LLM has the exact technical context needed to generate valid, idiomatic T code without trial-and-error.
T treats LLMs as first-class collaborators with structured boundaries:
t checkBefore an agent generates or modifies pipeline code, it should validate the result. T provides a tiered checking system that runs in seconds — no Nix builds required.
| Command | What it checks | Nix required? |
|---|---|---|
t check <file.t> |
Pipeline DAG structure, dependency cycles, node syntax | No |
t check --schema <file.t> |
+ column references, schema propagation | No |
REPL-callable versions (same options, returns
String):
result = t_check("src/pipeline.t") -- same as: t check
result = t_check("src/pipeline.t", schema=true) -- same as: t check --schema
result = t_diff("src/pipeline.t") -- same as: t diff
result = t_fix("src/pipeline.t") -- same as: t fix
t check --env <file.t> | +
tproject.toml declarations, lockfile consistency, Nix eval
| Yes |t check --json <file.t> | Structured JSON diagnostics
(works with any tier) | Depends on tier |The first two tiers are the workhorses for agent iteration. They catch structural errors and schema mismatches in seconds, letting agents fix problems before triggering expensive Nix builds.
Use --watch during active development for continuous
feedback:
t check --watch --schema src/pipeline.tThis runs immediately, then re-runs on every file save. Press Ctrl+C to stop. The exit code reflects the last check’s result, making it usable in CI and editor integrations.
t check --json emits machine-readable diagnostics:
{
"diagnostics": [
{
"error_class": "schema_mismatch",
"message": "Column 'score' not found in DataFrame. Did you mean 'scorer'?",
"node": "clean",
"span": { "start": [12, 5], "end": [12, 50] },
"suggested_fix": {
"kind": "rename_column",
"old_name": "score",
"new_name": "scorer",
"edit_distance": 1,
"is_unique": true
}
}
],
"exit_code": 1
}Each diagnostic includes:
error_class: categorizes the issue
(schema_mismatch, type_mismatch,
cycle_detected, etc.)node: the pipeline node where the issue was foundspan: source location [line, column]suggested_fix: a mechanical fix the agent can apply
(see t fix below)Agents consume this output to make targeted decisions — no parsing of human-readable error strings required.
t fixWhen t check --schema emits a
suggested_fix, the agent can apply it mechanically:
t fix src/pipeline.t -- apply fixes and rewrite the file
t fix --dry-run src/pipeline.t -- preview changes without modifyingCurrently supported fix types:
| Fix type | What it does |
|---|---|
| Rename_column | Renames $old_name column
references to $new_name throughout the file |
| Add_node_arg | Adds a missing argument to a node function call |
| Pin_package_version | Pins a dependency to a specific version in
tproject.toml |
The agent workflow is: run t check --schema, parse the
JSON output for suggested_fix entries, then run
t fix to apply them. Always review the diff before
committing.
t diffAfter iterative development, t diff compares the last
two Nix builds of a pipeline:
t diff src/pipeline.tIt reports per-node status:
| Status | Meaning |
|---|---|
| Unchanged | Same content hash in both builds |
| Changed | Different content hash |
| Added | Present in second build, absent in first |
| Removed | Present in first build, absent in second |
| Errored | Node failed in one or both builds |
For programmatic access, use diff_summary(p) in the
REPL:
diff_summary(p)
-- DataFrame with columns: name, status, hash_a, hash_b, class_a, class_b
This is useful for verifying that a code change only affected the intended nodes.
t test --jsonWhen agents modify code, they need to verify that existing tests
still pass. T provides structured test output via
t test --json:
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 --only "stats" --not "anova" # combine filters
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/
This returns a JSON object with test results:
{
"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:
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, enabling
consistent tooling across all T commands.
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
Intent blocks are machine-readable metadata that capture analytical goals.
intent {
description: "Analyze customer churn patterns by age group",
goal: "Identify age ranges with highest churn risk"
}
-- Analysis code follows...
intent {
-- High-level description
description: "Customer lifetime value segmentation",
goal: "Segment customers into value tiers for targeted marketing",
-- Data specifications
data_source: "customers.csv from CRM export 2023-12-31",
required_columns: ["customer_id", "total_spend", "purchase_count", "signup_date"],
-- Analytical assumptions
assumptions: [
"Churn defined as no purchase in 90 days",
"LTV calculated as total_spend / months_active",
"Test accounts excluded"
],
-- Business constraints
constraints: [
"Minimum 50 customers per segment",
"Segment labels: high_value, medium_value, low_value",
"Thresholds: high > $1000, medium > $500"
],
-- Data quality rules
validation: [
"total_spend > 0",
"purchase_count > 0",
"signup_date between 2020-01-01 and 2023-12-31"
],
-- Expected outputs
outputs: [
"customer_segments.csv with columns: customer_id, segment, ltv",
"segment_summary.csv with counts and average LTV per segment"
],
-- Metadata
created: "2024-01-15",
author: "Marketing Team",
llm_assistant: "GPT-4",
version: "1.0"
}
-- LLM generates implementation based on intent
analysis = pipeline {
raw = read_csv("customers.csv", clean_colnames = true)
validated = raw
|> filter($total_spend > 0)
|> filter($purchase_count > 0)
with_ltv = validated
|> mutate($ltv, \(row) row.total_spend / months_since(row.signup_date))
segmented = with_ltv
|> mutate($segment, \(row)
if (row.ltv > 1000) "high_value"
else if (row.ltv > 500) "medium_value"
else "low_value"
)
summary_result = segmented
|> group_by($segment)
|> summarize($count = nrow($segment), $avg_ltv = mean($ltv))
}
write_csv(analysis.segmented, "customer_segments.csv")
write_csv(analysis.summary_result, "segment_summary.csv")
Benefits:
Instead of generating entire scripts, LLMs generate pipeline nodes.
Human prompt: “Analyze sales by region”
LLM generates (entire script):
import pandas as pd
df = pd.read_csv("sales.csv")
df = df[df['amount'] > 0]
df_grouped = df.groupby('region')['amount'].sum()
df_grouped = df_grouped.sort_values(ascending=False)
print(df_grouped)Problems:
Human writes intent:
intent {
description: "Sales analysis by region",
steps: {
load: "Load sales.csv",
clean: "Remove zero/negative amounts",
analyze: "Sum revenue by region, sort descending"
}
}
LLM generates pipeline nodes:
analysis = pipeline {
-- Node 1: Load (stable)
raw = read_csv("sales.csv")
-- Node 2: Clean (can regenerate independently)
cleaned = raw |> filter($amount > 0)
-- Node 3: Analyze (can regenerate independently)
by_region = cleaned
|> group_by($region)
|> summarize($total = sum($amount))
|> arrange($total, "desc")
}
Benefits:
cleaned noderaw and by_region unchangedStep 1: Human writes intent
intent {
description: "Customer cohort analysis",
cohort_definition: "First purchase month",
metric: "Average order value by cohort",
timeframe: "2023-01-01 to 2023-12-31"
}
Step 2: LLM generates implementation
cohort_analysis = pipeline {
orders = read_csv("orders.csv")
-- LLM fills in details based on intent
}
Step 3: Human reviews, provides feedback
"Include only completed orders"
Step 4: LLM updates (localized change)
cleaned = orders |> filter($status == "completed")
Step 1: Human provides data sample
sample = read_csv("data.csv")
explain(sample)
-- DataFrame(100 rows x 5 cols: [date, product, region, quantity, price])
Step 2: Human requests analysis
"Calculate total revenue by product, show top 10"
Step 3: LLM generates with intent
intent {
description: "Top 10 products by revenue",
data: "data.csv with date, product, region, quantity, price",
computation: "revenue = quantity * price, group by product, sort descending, top 10"
}
top_products = sample
|> mutate($revenue = $quantity * $price)
|> group_by($product)
|> summarize($total_revenue = sum($revenue))
|> arrange($total_revenue, "desc")
|> head(10)
Iteration 1: Basic implementation
intent { description: "Average sales by month" }
monthly = sales |> group_by($month) |> summarize($avg = mean($amount))
Iteration 2: Add NA handling
intent {
description: "Average sales by month",
requirements: "Handle missing amounts"
}
monthly = sales |> group_by($month) |> summarize($avg = mean($amount))
Iteration 3: Add validation
intent {
description: "Average sales by month",
requirements: "Handle missing amounts, exclude zero sales"
}
monthly = sales
|> filter($amount > 0)
|> group_by($month)
|> summarize($avg = mean($amount))
Each iteration: Intent updated, LLM regenerates, human verifies.
This pattern uses T’s static checking tools to catch and fix errors before running the pipeline.
Step 1: Agent generates pipeline
analysis = pipeline {
raw = read_csv("sales.csv")
cleaned = raw
|> filter($amount > 0)
by_region = cleaned
|> group_by($region)
|> summarize($total = sum($amount))
}
Step 2: Run t check --schema to
validate
$ t check --schema src/pipeline.t
error [schema_mismatch] Column 'regin' not found. Did you mean 'region'?Step 3: Agent fixes the typo (localized change)
by_region = cleaned
|> group_by($region)
|> summarize($total = sum($amount))
Step 4: Run t check --schema again —
clean
$ t check --schema src/pipeline.t
$Step 5: Only now trigger the Nix build
$ t run src/pipeline.tThis loop — generate, check, fix, build — ensures agents never waste time on Nix builds that would fail for structural or schema reasons. The check step takes seconds; the build step takes minutes.
T provides introspection functions for LLM context:
df = read_csv("customers.csv")
explain(df)
-- "DataFrame(1000 rows x 5 cols: [id, name, age, city, ltv])"
-- JSON format for LLM consumption
explain_json(df)
-- {"type": "DataFrame", "rows": 1000, "columns": [...], "sample": [...]}
i = intent { description: "Analysis", goal: "Insights" }
intent_fields(i)
-- {description: "Analysis", goal: "Insights"}
intent_get(i, "description")
-- "Analysis"
p = pipeline {
x = 10
y = x * 2
z = y + 5
}
pipeline_nodes(p)
-- ["x", "y", "z"]
pipeline_deps(p, "z")
-- ["y"]
-- LLM can understand dependency graph
For machine consumption, agents use t check --json and
t fix --dry-run:
$ t check --json --schema src/pipeline.t
{
"diagnostics": [
{
"error_class": "schema_mismatch",
"message": "Column 'regin' not found. Did you mean 'region'?",
"node": "by_region",
"span": { "start": [12, 5], "end": [12, 60] },
"suggested_fix": {
"kind": "rename_column",
"old_name": "regin",
"new_name": "region",
"edit_distance": 1,
"is_unique": true
}
}
],
"exit_code": 1
}$ t fix --dry-run src/pipeline.t
dry-run: would apply 1 fix to src/pipeline.t:
[Rename_column] line 12: rename column 'regin' to 'region'$ t fix --dry-run src/pipeline.t
dry-run: would apply 1 fix to src/pipeline.t:
[Cast] line 12: insert mutate($amount = as.numeric($amount))These tools give agents structured, parseable output instead of human-readable error strings. The agent can programmatically decide what to fix and verify the fix before applying it.
t fix Diffs: Use
t fix --dry-run to preview changes before applying. Always
review the diff before committing.--watch: Run
t check --watch --schema during active development for
continuous feedbackna_rm,
validation checkst check First: Run
t check --schema before build_pipeline — it
catches structural and schema errors in seconds. In the REPL, use
t_check(file, schema=true) (same behavior, returns a
string).t fix for Mechanical Changes: When
t check --json reports a suggested_fix, apply
it with t fix rather than manually editing — it handles
line-number drift and word-boundary safety. In the REPL, use
t_fix(file).t diff: After modifying a
pipeline, run t diff to confirm only the intended nodes
changed. In the REPL, use t_diff(file).Human: “I need to analyze employee salaries by department and identify outliers”
LLM generates with intent:
intent {
description: "Employee salary analysis with outlier detection",
data_source: "employees.csv",
required_columns: ["employee_id", "department", "salary"],
analysis_steps: [
"Calculate statistics by department (mean, SD)",
"Identify outliers (>2 SD from department mean)",
"Generate summary report"
],
outlier_definition: "Salary > department_mean + 2*department_sd",
outputs: [
"department_stats.csv: mean, sd, count per department",
"outliers.csv: employees with salaries > 2 SD from mean"
],
created: "2024-07-12",
llm_assistant: "Claude"
}
salary_analysis = pipeline {
-- Load data
employees = read_csv("employees.csv", clean_colnames = true)
-- Department statistics
dept_stats = employees
|> group_by($department)
|> summarize($mean_salary = mean($salary),
$sd_salary = sd($salary),
$count = nrow($department))
-- Per-department outliers (>2 SD from department mean)
with_stats = employees
|> group_by($department)
|> mutate($dept_mean = mean($salary, na_rm = true))
|> mutate($dept_sd = sd($salary, na_rm = true))
outliers = with_stats
|> filter($salary > $dept_mean + 2 * $dept_sd)
|> select($employee_id, $department, $salary, $dept_mean, $dept_sd)
}
-- Export results
write_csv(salary_analysis.dept_stats, "department_stats.csv")
write_csv(salary_analysis.outliers, "outliers.csv")
LLM validates before building:
$ t check --schema src/pipeline.t
$Clean. The pipeline structure is valid. Schema propagation confirms all column references are resolvable.
LLM builds and runs:
$ t run src/pipeline.tHuman Review: “Good! But the outliers should only flag employees making more than $200k, not just 2 SD from mean”
LLM updates the outliers node
(localized change):
outliers = with_stats
|> filter($salary > 200000)
|> select($employee_id, $department, $salary, $dept_mean, $dept_sd)
LLM validates the change:
$ t check --schema src/pipeline.t
$Clean. The filter predicate changed but the column references are still valid.
LLM builds and verifies with
t diff:
$ t run src/pipeline.t
$ t diff src/pipeline.t
Name Status Class_a Class_b
employees Unchanged T T
dept_stats Unchanged T T
with_stats Unchanged T T
outliers Changed T TThe diff confirms only outliers changed.
employees, dept_stats, and
with_stats are unchanged — their cached artifacts are
reused.
Intent blocks + version control = complete audit trail:
git log --oneline intent_blocks/
abc123 Update: Exclude test accounts from churn analysis
def456 Add validation: minimum transaction amount $1
789ghi Initial: Customer churn analysis
git show abc123:src/pipeline.t
# Shows exactly what assumptions changed and whySee Also:
t debug for
interactive node debuggingt_check(), t_diff(), t_fix() REPL
functions and t check, t fix,
t diff CLI documentation