T Project Development Guide

This guide walks you through creating and developing a T project — a data analysis project that uses T packages.

Package vs Project: A package is a reusable library of T functions. A project is a data analysis workspace that depends on packages.

1. Creating a Project

Create a new project interactively:

$ t init --project housing-analysis
Initializing new T project...
Author [User]: Alice
License [EUPL-1.2]: EUPL-1.2
Nixpkgs date [2026-02-19]: 2026-02-19

✓ Project 'housing-analysis' created successfully!

1.1 AI Agent Onboarding (Optional)

When running t init, you will be prompted to select an AI Agent Context Level. This generates two essential files in your project root:

These files are designed to be read by LLMs (like Antigravity, Claude, or ChatGPT) at the start of a session to give them immediate, high-fidelity context about your project and the specific version of T you are using.

This creates the following structure:

2. Entering the Development Environment

Projects use Nix for reproducibility. Enter the development shell:

$ nix develop

This ensures all dependencies (T, packages, R, Nix) are available at the exact versions specified in flake.lock.

3. Adding Dependencies

Dependencies on T packages are declared in tproject.toml:

[project]
name = "housing-analysis"
description = "Analyzing housing data with T"

[dependencies]
my_stats = { git = "https://github.com/user/my-stats", tag = "v0.1.0" }
data_utils = { git = "https://github.com/user/data-utils", tag = "v0.2.0" }

[t]
min_version = "0.56.0"

Important: [dependencies] entries must be { git, tag } inline tables pointing to T packages. Version-constraint strings (e.g. tlang = ">=0.52.0") and array values (e.g. python = ["polars"]) are not valid and will produce a hard error from t update. To declare runtime-language packages, use the dedicated sections: - [r-dependencies].packages for R packages - [py-dependencies].packages for Python packages - [jl-dependencies].packages for Julia packages For the minimum T version, use [t].min_version.

3.1 System Dependencies and LaTeX

Beyond T packages, you can declare system-level tools and LaTeX packages required for your project.

Additional Tools

Use the [additional-tools] section to add any package from Nixpkgs (e.g., CLI utilities, compilers, or libraries):

[additional-tools]
# These will be available in your 'nix develop' shell and pipeline sandboxes
packages = ["git", "jq", "gawk", "pandoc"]

LaTeX Support

If your project involves generating PDFs or reports, use the [latex] section. T automatically provides a texlive environment starting from scheme-small. Just list any additional LaTeX packages you need:

[latex]
# Standard LaTeX packages
packages = ["amsmath", "blindtext", "physics", "hyperref"]

After adding or changing dependencies (including [additional-tools] or [latex] sections), run:

$ t update
Syncing 2 dependency(ies) from tproject.toml → flake.nix...
Running nix flake update...

This regenerates flake.nix so new dependencies and tools appear as proper flake inputs with locked versions. The tools will be available directly in your shell and automatically provided to any pipeline nodes during execution. When you declare runtime dependencies, the matching tlang companion package is also exposed in the project shell (library(tlang) for R, import tlang for Python, and using tlang for Julia). The R wrapper and both companion paths are present even with no declared dependencies: the Python package is stdlib-only, so import tlang works in a bare shell; the Julia package needs JSON, which t update auto-adds once any Julia dependency is declared. Then re-enter the shell:

$ nix develop

3.2 Upgrading T and Nixpkgs

To upgrade your project to the latest version of T and set the project’s nixpkgs date to today’s UTC date:

$ t upgrade
Checking for new T releases...
Upgrading project to T 0.54.3 and nixpkgs date 2026-07-27 (today's UTC date)...
Regenerating flake.nix and updating dependencies...
Running nix flake update...

This updates your tproject.toml and then runs t update automatically.

3.3 R Dependencies

R packages can be managed via the default nixpkgs resolver or by pointing T at an renv.lock file.

3.3.1 Nixpkgs Resolver (default)

List CRAN packages from nixpkgs directly:

[r-dependencies]
packages = ["dplyr", "ggplot2", "jsonlite"]

After editing, run t update to include them in flake.nix. Packages are available in every R pipeline node and in nix develop. On Linux, the same project R environment is also discovered by Positron as an R interpreter (re-run t update and launch Positron from nix develop).

3.3.1a Automatic Discovery from R Code

T scans R node code for package usage — roxygen @import/@importFrom tags, library()/require()/requireNamespace()/loadNamespace() calls, and pkg::fun qualifiers — and prompts you to add any missing packages to tproject.toml before building (or auto-adds them with TLANG_AUTO_ADD_PIPELINE_DEPS=1). Base packages are never listed. Names inside string literals and # comments are skipped. Discovery only ensures packages are installed; your code must still attach them (library(dplyr) or dplyr::mutate). For non-interactive runs, answer the prompt per command with t run --yes <file.t> (updates tproject.toml, like typing y) or t run --no <file.t> (declines, like typing N); --no always wins over --yes and the env vars, and --yes never skips a missing tproject.toml the way TLANG_AUTO_ADD_PIPELINE_DEPS=1 alone does.

#' @importFrom dplyr mutate filter
clean <- function(raw) {
  mutate(raw, x = 1)
}
# `t check --env` / build prompts: add "dplyr" to [r-dependencies].packages

3.3.2 renv Resolver

If your project already has an renv.lock file, set:

[r-dependencies]
resolver = "renv"

When resolver = "renv", T automatically discovers all R dependencies from renv.lock:

No packages list is required — renv.lock is the single source of truth. Run t update to regenerate flake.nix with the renv-discovered packages.

If a pipeline needs a package that is missing from renv.lock, t check --env reports an error with no auto fix. Update renv.lock by hand, then run t update. Or switch to renv+toml below. Caution: then renv.lock is no longer the single source of truth.

3.3.2b renv+toml Resolver (union)

To allow extra packages from tproject.toml without edit of renv.lock, set:

[r-dependencies]
resolver = "renv+toml"
packages = ["dplyr"]

When resolver = "renv+toml", T uses the union of renv.lock and tproject.toml:

3.3.3 Git R Packages

Declare R packages from remote Git repositories directly in tproject.toml:

[r-dependencies]
packages = ["dplyr"]
my_pkg = { git = "https://github.com/user/my-pkg", rev = "abc123def456" }

The rev field must be a full Git commit hash. Each git package is injected into every R pipeline node’s buildInputs.

When using resolver = "renv" or resolver = "renv+toml", git packages from renv.lock are automatically merged with those declared in tproject.toml.

3.4 Python Dependencies

Python packages can use the default nixpkgs resolver or the UV workspace resolver for PyPI-only dependencies.

3.4.1 Nixpkgs Resolver (default)

List packages from the pinned nixpkgs revision:

[py-dependencies]
packages = ["pandas", "scikit-learn"]

Run t update to generate a python.withPackages environment in flake.nix. Simple, no extra files needed.

3.4.2 UV Workspace Resolver

Projects that need packages from PyPI can opt into UV explicitly:

[py-dependencies]
resolver = "uv"
workspace = "python"

The version field is optional when using the UV resolver. If omitted, T infers the Nixpkgs Python attribute (e.g. python312) from the requires-python field in python/pyproject.toml. The inference accepts specifiers that constrain to a single minor version (==3.12, ==3.12.*, ~=3.12, >=3.12,<3.13) and errors on open-ended ranges (>=3.12). If an explicit version conflicts with requires-python, T prints a warning and uses the explicit value.

The workspace directory must contain the UV project metadata and lock file:

python/
  pyproject.toml
  uv.lock

When resolver = "uv", do not set [py-dependencies].packages; Python dependencies are declared only in pyproject.toml and locked by uv.lock. Running t update generates uv2nix/pyproject.nix inputs in flake.nix and builds the Python environment as a Nix virtual environment.

3.4.3 Setting up a UV workspace from scratch

This walkthrough assumes you do not have uv installed and have no existing Python package metadata — you are starting from an empty T project.

1. Create a new T project

t init --project my_project
cd my_project

2. Edit tproject.toml

Replace the default [py-dependencies] section (or add it if missing):

[py-dependencies]
resolver = "uv"
workspace = "python"

The version field is optional when using the UV resolver — T infers it from requires-python in pyproject.toml. The specifier must constrain Python to a single minor version (e.g. >=3.12,<3.13); an open-ended specifier like >=3.12 is rejected as ambiguous. Remove any packages key if present — UV and packages are mutually exclusive.

3. Create the Python workspace directory and pyproject.toml

mkdir python
# python/pyproject.toml
[project]
name = "my_project_python_env"
version = "0.1.0"
requires-python = ">=3.12,<3.13"
dependencies = [
    "pandas",
]

Add any PyPI packages to the dependencies list. Re-run uv lock whenever the list changes.

4. Generate the lock file

You need uv and python3 on PATH. If you do not have them installed, enter a temporary Nix shell:

nix shell nixpkgs#uv nixpkgs#python3

Then generate the lock file:

uv lock --project python

Type exit or press Ctrl-D to leave the Nix shell when done, or keep it open for the next step.

5. Generate the Nix flake

t update

This reads the UV workspace, adds pyproject-nix, uv2nix, and pyproject-build-systems inputs to flake.nix, and configures the Python environment to use pySet.mkVirtualEnv instead of pkgs.python314.withPackages.

6. Run your pipeline

nix develop
t run src/pipeline.t

Python pipeline nodes work identically regardless of which resolver you chose.

Adding or removing dependencies later

  1. Edit dependencies in python/pyproject.toml.
  2. Re-run uv lock --project python.
  3. Re-run t update to regenerate flake.nix.
  4. Commit the updated pyproject.toml and uv.lock.

3.5 Julia Dependencies

Julia packages use the nixpkgs resolver. List them under [jl-dependencies]:

[jl-dependencies]
version = "lts"
packages = ["DataFrames", "CSV", "GLM"]

The version field defaults to "lts" (the current Julia long-term-support release). To pin a specific version, use the format "1.10" or "1.11", which maps to the corresponding julia_1_10 or julia_1_11 attribute in nixpkgs.

After editing, run t update to include the Julia packages in flake.nix. They are then available via using inside jln() nodes:

p = pipeline {
  julia_step = jln(
    command = <{
      using DataFrames, CSV
      df = CSV.read("data.csv", DataFrame)
      println(nrow(df))
    }>
  )
}

The tlang companion package (for debug_node, read_node helpers, etc.) is automatically injected into every Julia node — no need to declare it.

3.6 Pipeline Options

[pipeline]
# Record embedded node source in build logs for show_code() (default: true).
# Set to false when node code holds secrets. Script paths are still recorded.
record_source = true

By default, build logs record each node’s source code so companions can show it with show_code(). Exterior script = nodes record only their path plus a content hash. Set [pipeline].record_source = false to keep embedded code (and any secrets in it) out of _pipeline/ build logs.

4. Importing Packages

Once inside nix develop, you can use the import statement in your T scripts to load package functions.

Import All Public Functions

import my_stats

This makes all public functions from my_stats available in scope.

Import Specific Functions

import my_stats[weighted_mean, correlation]

Only weighted_mean and correlation are imported.

Import with Aliases

import my_stats[wmean=weighted_mean, cor=correlation]

weighted_mean is available as wmean, correlation as cor.

Visibility

All functions in a package are public by default. Package authors can mark internal helpers as private using @private in T-Doc comments — those functions will not be visible to importers.

5. Writing Analysis Scripts

Write your analysis in src/. For example, src/pipeline.t:

import my_stats
import data_utils[read_clean]

p = pipeline {
  data = read_csv("data/housing.csv")
  clean = read_clean(data)
  avg_price = mean(clean.$price)
  price_by_area = clean |>
    group_by($area) |>
    summarize(avg=mean($price), sd=sd($price))
}

build_pipeline(p)
print(price_by_area)

Run your script:

$ t run src/pipeline.t

Or use the REPL for interactive exploration:

$ t repl

6. Running Tests

You can add tests in tests/ following the same conventions as packages:

-- tests/test-pipeline.t
import my_stats

result = weighted_mean([1, 2, 3], [0.5, 0.3, 0.2])
assert(result == 1.7)

Run them with:

$ t test

6.1 Pipeline Verification Nodes & Assertion Dictionaries

When testing pipeline workflows, separate data transformation nodes from check/assertion nodes. In verification nodes, return a named dictionary of assert(expect_*(...)) calls serialized as JSON (serializer = ^json):

test_filter = pipeline {
  -- Data transformation node (produces CSV filtered_data)
  filtered_data = node(
    command = data |> filter($score >= 88),
    serializer = ^csv,
    deserializer = ^csv
  )
  -- Test node (returns named Dict of assertion results)
  -- On success: serializes [ nrow_check: true, colnames_check: true ] as JSON
  -- On failure: assert short-circuits and records VError failure object in build log
  check_filter = node(
    command = [
      nrow_check: assert(expect_nrow(filtered_data, 2)),
      colnames_check: assert(expect_colnames(filtered_data, ["name", "score", "grade"]))
    ],
    serializer = ^json,
    deserializer = ^csv
  )
}

This pattern provides a clean duality:

Skipping Pipeline Tests Conditionally

Since pipeline nodes are compiled and executed via Nix, tests that build or run pipelines might fail or block in sandboxed environments or systems lacking Nix. You can conditionally skip the execution of specific pipeline nodes using the noop parameter with any T expression.

Because noop accepts T expressions (evaluated at runtime), you can pass conditions referencing environment variables:

import my_project

p = pipeline {
  heavy_node = node(
    command = <{ run_heavy_nix_job() }>,
    # Skip execution if not running in CI
    noop = (get(env("CI"), "") == "")
  )
}

res = build_pipeline(p)

# Because errors are first-class values in T, skipped nodes propagate VError values cleanly.
# You can check if the node was skipped using expect_error or custom checks:
assert(expect_error(read_node(res.heavy_node), class = "TypeError", message = "was skipped"))

7. Reproducibility

Your project is fully reproducible through Nix:

Anyone can reproduce your environment:

$ git clone https://github.com/user/housing-analysis
$ cd housing-analysis
$ nix develop
$ t run src/pipeline.t

The same T version, same package versions, same R packages, and same system libraries are used every time.

8. IDE Support & Autocompletion

T projects are designed to work seamlessly with modern editors via the Language Server Protocol (LSP).

Using the LSP

The t-lsp binary is provided automatically by your project’s nix develop shell.

  1. Configure your editor (Vim, Emacs, or VS Code) once following the Editor Support Guide.
  2. Launch your editor inside the project directory after running nix develop.
  3. Alternatively, use direnv to automatically load the environment when you enter the project folder.

Using the Atelier TUI IDE

T projects also support Atelier, a tmux-based TUI IDE that provides a split-pane environment with an embedded editor, REPL, file tree, variable watcher, pipeline diagram, and plot viewer. To enable Atelier in a new project, pass --include-atelier to t init. Once inside nix develop, simply run atelier.

Once active, you will get real-time autocompletion for:


Next Steps

  1. Language Overview — Learn about types, syntax, and logic.
  2. Pipeline Tutorial — Learn how to structure your analysis as a DAG.
  3. Serializers — Understand data interchange between T, R, Python, and Julia nodes.
  4. Data I/O & Formats — Read and write CSV, Parquet, and Arrow IPC files.
  5. API Reference — Explore the standard library.
  6. Data Manipulation Examples — More worked examples of data wrangling.