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.
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!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:
pipeline.t)..claude/skills/t-project/SKILL.md: An
AI agent skill file that teaches LLMs how to work with T projects
(scaffolded automatically).Projects use Nix for reproducibility. Enter the development shell:
$ nix developThis ensures all dependencies (T, packages, R, Nix) are available at
the exact versions specified in flake.lock.
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.55.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 fromt update. To declare runtime-language packages, use the dedicated sections: -[r-dependencies].packagesfor R packages -[py-dependencies].packagesfor Python packages -[jl-dependencies].packagesfor Julia packages For the minimum T version, use[t].min_version.
Beyond T packages, you can declare system-level tools and LaTeX packages required for your project.
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"]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). Then re-enter the shell:
$ nix developTo 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.
R packages can be managed via the default nixpkgs resolver or by
pointing T at an renv.lock file.
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.
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:
Repository or Bioconductor field and mapped to
pkgs.rPackages.*.builtins.fetchGit using the RemoteHost,
RemoteUsername, RemoteRepo, and
RemoteSha fields.Remotes field is parsed: entries
like user/repo are matched against packages in the lock
file and injected as buildInputs of the declaring git
package.api.github.com and
gitlab.com. Other sources (bitbucket, custom URLs) produce
a warning and are skipped.R, methods,
stats, etc.) are automatically filtered out.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.
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", git packages from
renv.lock are automatically merged with those declared in
tproject.toml.
Python packages can use the default nixpkgs resolver or the UV workspace resolver for PyPI-only dependencies.
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.
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.
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_project2. 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#python3Then generate the lock file:
uv lock --project pythonType 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 updateThis 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.tPython pipeline nodes work identically regardless of which resolver you chose.
Adding or removing dependencies later
dependencies in
python/pyproject.toml.uv lock --project python.t update to regenerate
flake.nix.pyproject.toml and
uv.lock.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.
Once inside nix develop, you can use the
import statement in your T scripts to load package
functions.
import my_stats
This makes all public functions from my_stats available
in scope.
import my_stats[weighted_mean, correlation]
Only weighted_mean and correlation are
imported.
import my_stats[wmean=weighted_mean, cor=correlation]
weighted_mean is available as wmean,
correlation as cor.
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.
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.tOr use the REPL for interactive exploration:
$ t replYou 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 testWhen 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:
true (accessible via
read_node(p.check_filter)).AssertionError object in
the node’s build log.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"))
Your project is fully reproducible through Nix:
flake.nix declares exact dependency
sources (managed by t update)flake.lock pins exact versions of all
inputstproject.toml is the human-readable
source of truth for dependenciesAnyone can reproduce your environment:
$ git clone https://github.com/user/housing-analysis
$ cd housing-analysis
$ nix develop
$ t run src/pipeline.tThe same T version, same package versions, same R packages, and same system libraries are used every time.
T projects are designed to work seamlessly with modern editors via the Language Server Protocol (LSP).
The t-lsp binary is provided automatically by your
project’s nix develop shell.
nix develop.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:
$ prefix).