LaminDB: data management for traceable, multimodal AI
¶
LaminDB is an open-source data management tool that makes it easy to query, trace & govern datasets across diverse storage formats and locations. It gives you context through annotations, memory through lineage, and governance through branching and versioning. It uses a scalable lakehouse architecture that integrates files, tables, arrays, ontologies, and notes. It supports biological formats and registries by the creators of Scanpy.
Why?
Untraceable results cannot be trusted, especially in the age of agents.
Without effective access to multimodal data, models burn tokens or fail entirely.
We want to govern changes to data like we govern changes to code with git.
Especially in life sciences, teams need end-to-end traceability for GxP compliance (21 CFR Part 11 and EU Annex 11).
How?
lineage → trace results across agent sessions, notebooks, scripts & workflows
lakehouse → query across many datasets, manage tables & arrays schema-based and ACID
LIMS & ELN → unified schema-based records management with support for ontologies & notes
FAIR datasets → validate & annotate files,
DataFrame,AnnData,SpatialData,zarr, …governance → manage changes via branching & by versioning data + code
Architecture?
zero lock-in → uses open standards (metadata in SQLite/Postgres, data in
parquet,zarr, etc.)scalable → hit storage & database directly through your
pydataor R stack, no REST API involvedsimple →
pip install lamindborinstall.packages('laminr')- no Docker required, no separate backendunified → federate data across storage locations (local, S3, GCP, …) in any database
distributed → federate data zero-copy & lineage-aware across databases
reproducible → track agent traces, source code & compute environments
idempotent → re-run logic without worries about duplications or overwrites
integrations → bio ontologies git, nextflow, vitessce, redun, and more
extensible → create custom plug-ins based on the Django ORM, the basis for LaminDB’s registries
Who?
Scientists and engineers at leading research institutions and biotech companies, including:
Industry → Pfizer, Altos Labs, Ensocell Therapeutics, …
Academia & Research → scverse, DZNE (National Research Center for Neuro-Degenerative Diseases), Helmholtz Munich (National Research Center for Environmental Health), …
Research Hospitals → Global Immunological Swarm Learning Network: Harvard, MIT, Stanford, ETH Zürich, Charité, U Bonn, Mount Sinai, …
From personal research projects to pharma-scale deployments managing petabytes of data across:
entities |
OOMs |
|---|---|
observations & datasets |
10¹² & 10⁶ |
runs & transforms |
10⁹ & 10⁵ |
proteins & genes |
10⁹ & 10⁶ |
biosamples & species |
10⁵ & 10² |
… |
… |
UI, permissions, audit logs? LaminHub is a collaboration hub built on LaminDB similar to how GitHub is built on git.
Quickstart¶
To install the Python package with recommended dependencies, use:
pip install lamindb
Install with minimal dependencies.
The lamindb package adds data-science related dependencies through the [full] extra, see here.
For a minimal install of the lamindb namespace, use:
pip install lamindb-core
Agent? The lamindb skill ships with the lamindb package at .agents/skills/. Pure markdown docs ship with the package in docs/ and are available from the web via llms.txt.
Query databases & datasets¶
You can browse public databases at lamin.ai/explore. To access laminlabs/cellxgene, run:
import lamindb as ln
db = ln.DB("laminlabs/cellxgene") # a database object for queries
df = db.Artifact.to_dataframe() # a dataframe listing datasets & models
→ connected lamindb: anonymous/test-readme
! truncated query result to limit=20 Artifact objects
To get a specific dataset, run:
artifact = db.Artifact.get("BnMwC3KZz0BuKftR") # a metadata object for a dataset
artifact.describe() # describe the context of the dataset
Artifact: cell-census/2025-11-08/h5ads/82346769-8733-485e-ab49-f14923d2b5bc.h5ad (2025-11-08) | description: OPCs ├── uid: BnMwC3KZz0BuKftR0001 run: 7FgSsR6 (annotate-register-new-release.py) │ kind: None otype: AnnData │ hash: hu09QNaDv3RLVyvrAlOFfg size: 63.2 MB │ branch: main space: all │ created_at: 2026-02-17 13:38:30 UTC created_by: zethson │ n_observations: 3324.0 schema: CELLxGENE AnnData of ontology_id ├── storage/path: s3://cellxgene-data-public/cell-census/2025-11-08/h5ads/82346769-8733-485e-ab49-f14923d2b5bc.h5ad ├── Dataset features │ ├── obs (11.0) │ │ assay_ontology_term_id bionty.ExperimentalFactor.ontology… EFO:0009922 │ │ cell_type_ontology_term_id bionty.CellType.ontology_id CL:0002453 │ │ development_stage_ontology_t… bionty.DevelopmentalStage.ontology… HsapDv:0000147, HsapDv:0000162, HsapDv… │ │ disease_ontology_term_id bionty.Disease.ontology_id MONDO:0004975, MONDO:0800027, PATO:000… │ │ donor_id str │ │ is_primary_data ULabel │ │ self_reported_ethnicity_onto… bionty.Ethnicity.ontology_id HANCESTRO:0568, HANCESTRO:0590, unknown │ │ sex_ontology_term_id bionty.Phenotype.ontology_id PATO:0000383, PATO:0000384 │ │ suspension_type ULabel nucleus │ │ tissue_ontology_term_id bionty.Tissue.ontology_id|bionty.C… UBERON:0000451, UBERON:0016528, UBERON… │ │ tissue_type ULabel tissue │ ├── uns (1.0) │ │ organism_ontology_term_id bionty.Organism.ontology_id NCBITaxon:9606 │ └── var (2.0) │ feature_is_filtered bool │ var_index bionty.Gene.ensembl_gene_id[source… └── Labels └── .recreating_runs Run 2026-02-17 13:43:36.195894+00:00, 2026… .ulabels ULabel nucleus, tissue .organisms bionty.Organism human .tissues bionty.Tissue prefrontal cortex, white matter of fro… .cell_types bionty.CellType oligodendrocyte precursor cell .diseases bionty.Disease Alzheimer disease, leukoencephalopathy… .phenotypes bionty.Phenotype female, male .experimental_factors bionty.ExperimentalFactor 10x 3' v3 .developmental_stages bionty.DevelopmentalStage 81-year-old stage, 53-year-old stage, … .ethnicities bionty.Ethnicity African American, unknown, European Am…
See the output.
Access the content of the dataset via:
local_path = artifact.cache() # return a local path from a cache
adata = artifact.load() # load object into memory
! run input wasn't tracked, call `ln.track()` and re-run
! run input wasn't tracked, call `ln.track()` and re-run
For broader queries of cellxgene, see docs.lamin.ai/cellxgene.
Save files & folders¶
You can create a LaminDB instance at lamin.ai and invite collaborators. To connect to an existing instance, run:
lamin login
lamin connect account/name # tip: add flag `--here` to scope to current directory
Or init a new instance instead (no login required).
lamin init --storage ./quickstart-data --modules bionty
On the terminal and in a Python session, LaminDB will now auto-connect. For more configuration, see docs.lamin.ai/setup.
To save a file or folder via the API:
import lamindb as ln
# → connected lamindb: account/instance
open("sample.fasta", "w").write(">seq1\nACGT\n") # create dataset
ln.Artifact("sample.fasta", key="sample.fasta").save() # save dataset
! no run & transform got linked, call `ln.track()` & re-run
! calling anonymously, will miss private instances
Artifact(uid='DJuc2fG1vdkKomLs0000', key='sample.fasta', description=None, suffix='.fasta', kind=None, otype=None, size=11, hash='83rEPcAoBHmYiIuyBYrFKg', n_files=None, n_observations=None, extra_data=None, branch_id=1, created_on_id=1, space_id=1, storage_id=1, run_id=None, schema_id=None, created_by_id=1, created_at=2026-08-07 16:20:31 UTC, is_locked=False, version_tag=None, is_latest=True)
To save a file or folder via the CLI, run:
lamin save sample.fasta --key sample.fasta
To load an artifact via the CLI into a local cache, run:
lamin load --key sample.fasta
Read more about the CLI: docs.lamin.ai/cli.
Trace data, code & agents¶
The lamindb skill ships with the lamindb package at .agents/skills/. Ask your coding agent to copy it to wherever it reads skills from — .claude/skills/ for Claude Code, .agents/skills/ for GitHub Copilot — so that it automatically tracks agent sessions.
To create a dataset in a script or notebook while tracking source code, inputs, outputs, logs, and environment:
import lamindb as ln
# → connected lamindb: account/instance
ln.track() # track code execution
open("sample.fasta", "w").write(">seq1\nACGT\n") # create dataset
ln.Artifact("sample.fasta", key="sample.fasta").save() # save dataset
ln.finish() # mark run as finished
→ created Transform('iI6Kty6mg6jF0000', key='README.ipynb'), started new Run('RXwpzbmf2n1Ylbdk') at 2026-08-07 16:20:31 UTC
→ notebook imports: anndata==0.13.2 bionty==2.4.3 lamindb numpy==2.5.1 pandas==2.3.3
• tip: to identify the notebook across renames, pass the uid: ln.track("iI6Kty6mg6jF")
→ returning artifact with same hash: Artifact(uid='DJuc2fG1vdkKomLs0000', key='sample.fasta', description=None, suffix='.fasta', kind=None, otype=None, size=11, hash='83rEPcAoBHmYiIuyBYrFKg', n_files=None, n_observations=None, extra_data=None, branch_id=1, created_on_id=1, space_id=1, storage_id=1, run_id=None, schema_id=None, created_by_id=1, created_at=2026-08-07 16:20:31 UTC, is_locked=False, version_tag=None, is_latest=True); to track this artifact as an input, use: ln.Artifact.get()
! run was not set on Artifact(uid='DJuc2fG1vdkKomLs0000', key='sample.fasta', description=None, suffix='.fasta', kind=None, otype=None, size=11, hash='83rEPcAoBHmYiIuyBYrFKg', n_files=None, n_observations=None, extra_data=None, branch_id=1, created_on_id=1, space_id=1, storage_id=1, run_id=None, schema_id=None, created_by_id=1, created_at=2026-08-07 16:20:31 UTC, is_locked=False, version_tag=None, is_latest=True), setting to current run
! cells [(4, 6), (8, 11), (22, 24)] were not run consecutively
→ finished Run('RXwpzbmf2n1Ylbdk') after 2s at 2026-08-07 16:20:34 UTC
Running this snippet as a script (python create_fasta.py) produces the following data lineage:
artifact = ln.Artifact.get(key="sample.fasta") # get artifact by key
artifact.describe() # context of the artifact
artifact.view_lineage() # fine-grained lineage
Artifact: sample.fasta (0000) ├── uid: DJuc2fG1vdkKomLs0000 run: RXwpzbm (README.ipynb) │ hash: 83rEPcAoBHmYiIuyBYrFKg size: 11 B │ branch: main space: all │ created_at: 2026-08-07 16:20:31 UTC created_by: anonymous └── storage/path: /home/runner/work/lamindb/lamindb/test-readme/.lamindb/DJuc2fG1vdkKomLs0000.fasta

Watch a mini video: youtu.be/yK3ODFZLL1A
Access run & transform.
run = artifact.run # get the run object
transform = artifact.transform # get the transform object
run.describe() # context of the run
LaminDB is an open-source data management tool that makes it easy to query, trace & govern datasets across diverse storage formats and locations. It gives you context through annotations, memory through lineage, and governance through branching and versioning. It uses a scalable lakehouse architecture that integrates files, tables, arrays, ontologies, and notes. It supports biological formats and registries by the creators of Scanpy.
Why?
- Untraceable results cannot be trusted, especially in the age of agents.
- Without effective access to multimodal data, models burn tokens or fail entirely.
- We want to govern changes to data like we govern changes to code with git.
Especially in life sciences, teams need end-to-end traceability for GxP compliance (21 CFR Part 11 and EU Annex 11).
How?
- lineage → trace results across agent sessions, notebooks, scripts & workflows
- lakehouse → query across many datasets, manage tables & arrays schema-based and ACID
- LIMS & ELN → unified schema-based records management with support for ontologies & notes
- FAIR datasets → validate & annotate files,
DataFrame,AnnData,SpatialData,zarr, … - governance → manage changes via branching & by versioning data + code
Architecture?
- zero lock-in → uses open standards (metadata in SQLite/Postgres, data in
parquet,zarr, etc.) - scalable → hit storage & database directly through your
pydataor R stack, no REST API involved - simple →
pip install lamindborinstall.packages('laminr')- no Docker required, no separate backend - unified → federate data across storage locations (local, S3, GCP, …) in any database
- distributed → federate data zero-copy & lineage-aware across databases
- reproducible → track agent traces, source code & compute environments
- idempotent → re-run logic without worries about duplications or overwrites
- integrations → bio ontologies git, nextflow, vitessce, redun, and more
- extensible → create custom plug-ins based on the Django ORM, the basis for LaminDB's registries
Who?
Scientists and engineers at leading research institutions and biotech companies, including:
- Industry → Pfizer, Altos Labs, Ensocell Therapeutics, ...
- Academia & Research → scverse, DZNE (National Research Center for Neuro-Degenerative Diseases), Helmholtz Munich (National Research Center for Environmental Health), ...
- Research Hospitals → Global Immunological Swarm Learning Network: Harvard, MIT, Stanford, ETH Zürich, Charité, U Bonn, Mount Sinai, ...
From personal research projects to pharma-scale deployments managing petabytes of data across:
| entities | OOMs |
|---|---|
| observations & datasets | 10¹² & 10⁶ |
| runs & transforms | 10⁹ & 10⁵ |
| proteins & genes | 10⁹ & 10⁶ |
| biosamples & species | 10⁵ & 10² |
| ... | ... |
UI, permissions, audit logs? LaminHub is a collaboration hub built on LaminDB similar to how GitHub is built on git.
Quickstart
To install the Python package with recommended dependencies, use:
pip install lamindb
Install with minimal dependencies.
The lamindb package adds data-science related dependencies through the [full] extra, see here.
For a minimal install of the lamindb namespace, use:
pip install lamindb-core
Agent? The lamindb skill ships with the lamindb package at .agents/skills/. Pure markdown docs ship with the package in docs/ and are available from the web via llms.txt.
Query databases & datasets
You can browse public databases at lamin.ai/explore. To access laminlabs/cellxgene, run:
import lamindb as ln
db = ln.DB("laminlabs/cellxgene") # a database object for queries
df = db.Artifact.to_dataframe() # a dataframe listing datasets & models
To get a specific dataset, run:
artifact = db.Artifact.get("BnMwC3KZz0BuKftR") # a metadata object for a dataset
artifact.describe() # describe the context of the dataset
See the output.
Access the content of the dataset via:
local_path = artifact.cache() # return a local path from a cache
adata = artifact.load() # load object into memory
For broader queries of cellxgene, see docs.lamin.ai/cellxgene.
Save files & folders
You can create a LaminDB instance at lamin.ai and invite collaborators. To connect to an existing instance, run:
lamin login
lamin connect account/name # tip: add flag `--here` to scope to current directory
Or init a new instance instead (no login required).
lamin init --storage ./quickstart-data --modules bionty
On the terminal and in a Python session, LaminDB will now auto-connect. For more configuration, see docs.lamin.ai/setup.
To save a file or folder via the API:
import lamindb as ln
# → connected lamindb: account/instance
open("sample.fasta", "w").write(">seq1\nACGT\n") # create dataset
ln.Artifact("sample.fasta", key="sample.fasta").save() # save dataset
To save a file or folder via the CLI, run:
lamin save sample.fasta --key sample.fasta
To load an artifact via the CLI into a local cache, run:
lamin load --key sample.fasta
Read more about the CLI: docs.lamin.ai/cli.
Trace data, code & agents
The lamindb skill ships with the lamindb package at .agents/skills/. Ask your coding agent to copy it to wherever it reads skills from — .claude/skills/ for Claude Code, .agents/skills/ for GitHub Copilot — so that it automatically tracks agent sessions.
To create a dataset in a script or notebook while tracking source code, inputs, outputs, logs, and environment:
import lamindb as ln
# → connected lamindb: account/instance
ln.track() # track code execution
open("sample.fasta", "w").write(">seq1\nACGT\n") # create dataset
ln.Artifact("sample.fasta", key="sample.fasta").save() # save dataset
ln.finish() # mark run as finished
Running this snippet as a script (python create_fasta.py) produces the following data lineage:
artifact = ln.Artifact.get(key="sample.fasta") # get artifact by key
artifact.describe() # context of the artifact
artifact.view_lineage() # fine-grained lineage
run = artifact.run # get the run object
transform = artifact.transform # get the transform object
run.describe() # context of the run

transform.describe() # context of the transform

Track a project or an agent plan.
Pass a project/artifact to ln.track(), for example:
Note that you have to create a project or save the agent plan in case they don't yet exist:
# create a project with the CLI
lamin create project "My project"
# save an agent plan with the CLI
lamin save /path/to/.cursor/plans/curate-dataset-x.plan.md
lamin save /path/to/.claude/plans/curate-dataset-x.md
Or in Python:
You can track workflows by decorating functions:
import lamindb as ln
@ln.flow()
def create_fasta(fasta_file: str = "sample.fasta"):
open(fasta_file, "w").write(">seq1\nACGT\n") # create dataset
ln.Artifact(fasta_file, key=fasta_file).save() # save dataset
if __name__ == "__main__":
pass
Beyond what you get for scripts & notebooks, this automatically tracks function & CLI params and integrates well with established Python workflow managers: docs.lamin.ai/track. To integrate advanced bioinformatics pipeline managers like Nextflow, see docs.lamin.ai/pipelines.
A richer example.
Here is an automatically generated re-construction of the project of Schmidt et al. (Science, 2022):
A phenotypic CRISPRa screening result is integrated with scRNA-seq data. Here is the result of the screen input:
Label artifacts
You can label an artifact by running:
my_label = ln.ULabel(name="My label").save() # a universal label
project = ln.Project(name="My project").save() # a project label
artifact.ulabels.add(my_label)
artifact.projects.add(project)
Query for it:
ln.Artifact.filter(ulabels=my_label, projects=project).to_dataframe()
You can also query by the metadata that lamindb automatically collects:
ln.Artifact.filter(run=run).to_dataframe() # by creating run
ln.Artifact.filter(transform=transform).to_dataframe() # by creating transform
ln.Artifact.filter(size__gt=1e6).to_dataframe() # size greater than 1MB
If you want to include more information into the resulting dataframe, pass include.
ln.Artifact.to_dataframe(include=["created_by__name", "storage__root"]) # include fields from related registries
The query syntax for DB objects and for your default database is the same.
Here is an overview that illustrates how artifacts can be labeled by other entities:
Read more: docs.lamin.ai/organize.
Manage features & records
Let's define some features:
from datetime import date
gc_content = ln.Feature(name="gc_content", dtype=float).save()
experiment_note = ln.Feature(name="experiment_note", dtype=str).save()
experiment_date = ln.Feature(name="experiment_date", dtype=date, coerce=True).save() # accept date strings
The most basic thing you can do with features is annotating artifacts, records, or runs with them:
artifact.features.set_values({
gc_content: 0.55,
experiment_note: "Looks great",
experiment_date: "2025-10-24",
})
# query
ln.Artifact.filter(experiment_date == "2025-10-24").to_dataframe(include="features") # query all artifacts annotated with `experiment_date`
You can create records for entities underlying your experiments (samples, perturbations, instruments, etc.):
ln.Record(name="Sample 1", features={gc_content: 0.5}).save()
You can dynamically create registries and relationships of entities via record types:
# create an experiments registry by defining a record type
experiments_registry = ln.Record(name="Experiments", is_type=True).save()
# create a record inside the Experiments registry
ln.Record(name="Experiment 1", type=experiments_registry).save()
# create a feature that links experiments, creating a relationship
experiment = ln.Feature(name="experiment", dtype=experiments_registry).save()
# create a sample record that links the sample to `Experiment 1` via the `experiment` feature
ln.Record(name="Sample 2", features={gc_content: 0.5, experiment: "Experiment 1"}).save()
# export a registry to a dataframe
experiments_registry.to_dataframe()
import pandas as pd
df = pd.DataFrame({
"sequence_str": ["ACGT", "TGCA"],
"gc_content": [0.55, 0.54],
"experiment_note": ["Looks great", "Ok"],
"experiment_date": [date(2025, 10, 24), date(2025, 10, 25)],
})
ln.Artifact.from_dataframe(df, key="my_datasets/sequences.parquet").save() # no validation
To validate & annotate the content of the dataframe, use the built-in schema valid_features:
ln.Feature(name="sequence_str", dtype=str).save() # define a remaining feature
artifact = ln.Artifact.from_dataframe(
df,
key="my_datasets/sequences.parquet",
schema="valid_features" # validate columns against features
).save()
artifact.describe()
Watch a mini video: youtu.be/Ji6E7hTnReQ
You can filter for datasets by schema and then launch distributed queries or batch load distributed datasets. For tables, see: docs.lamin.ai/tables. For arrays, see: docs.lamin.ai/arrays.
To validate an AnnData, call:
import anndata as ad
import numpy as np
import pandas as pd
adata = ad.AnnData(
X=np.ones((21, 10)),
obs=pd.DataFrame({'cell_type_by_model': ['T cell', 'B cell', 'NK cell'] * 7}),
var=pd.DataFrame(index=[f'ENSG{i:011d}' for i in range(10)])
)
artifact = ln.Artifact.from_anndata(
adata,
key="my_datasets/scrna.h5ad",
schema="ensembl_gene_ids_and_valid_features_in_obs"
).save()
artifact.describe()
To validate a SpatialData or any other array-like dataset, you need to construct a Schema. You can do this by composing simple pandera-style schemas: docs.lamin.ai/curate.
Branching & versioning
LaminDB co-versions code and datasets for you.
If edit and run the create_fasta.py script, you'll automatically create a new version of the transform and the sample.fasta artifact.
The edited script
# create_fasta.py
import lamindb as ln
ln.track()
open("sample.fasta", "w").write(">seq1\nTGCA\n") # a new sequence
ln.Artifact("sample.fasta", key="sample.fasta", features={"experiment": "Experiment 1"}).save() # annotate with the new experiment
ln.finish()
artifact_latest = ln.Artifact.get(key="sample.fasta") # pass version for a previous version: ln.Artifact.get(key="sample.fasta", version="1.0")
artifact_latest.versions.to_dataframe() # all versions of that artifact
artifact_latest.transform.versions.to_dataframe() # all versions of the transform that created the artifact
To isolate changes, create a contribution branch and switch to it as in git:
lamin switch -c my_branch
To merge a contribution branch into main, run:
lamin switch main # switch to the main branch
lamin merge my_branch # merge contribution branch into main
Read more: docs.lamin.ai/manage-changes.
Watch a mini video: youtu.be/rzRwcMj6-fc
Data sharing
To share data in a lineage-aware way, transfer objects from a source database to your default database:
db = ln.DB("laminlabs/lamindata")
artifact = db.Artifact.get(key="example_datasets/mini_immuno/dataset1.h5ad")
artifact.save()
This is zero-copy for the artifact's data in storage. Read more: docs.lamin.ai/transfer.
Ontologies
Plugin bionty gives you >20 public ontologies as SQLRecord registries. This was used to validate the ENSG ids in the adata just before.
import bionty as bt
bt.CellType.import_source() # import the default ontology
bt.CellType.to_dataframe() # your extensible cell type ontology in a simple registry
You can then create objects, e.g. for labeling, analogous to ULabel, Project, or Record:
t_cell = bt.CellType.get(name="T cell")
artifact.cell_types.add(t_cell)
Read more: docs.lamin.ai/manage-ontologies.
Watch a mini video: youtu.be/3vpWjHj3Kw8
Manage notes
When in your development directory, you can save markdown files as records:
lamin save <topic>/<my-note.md>
Run: RXwpzbm (README.ipynb) ├── uid: RXwpzbmf2n1Ylbdk transform: README.ipynb (0000) │ started_at: 2026-08-07 16:20:31 UTC finished_at: 2026-08-07 16:20:34 UTC │ status: completed │ branch: main space: all │ created_at: 2026-08-07 16:20:31 UTC created_by: anonymous └── environment: dpZzKlI │ aiobotocore==3.9.0 │ aiohappyeyeballs==2.7.1 │ aiohttp==3.14.3 │ aioitertools==0.13.0 │ …

transform.describe() # context of the transform
Transform: README.ipynb (0000) | description: LaminDB: data management for traceable, multimodal AI ├── uid: iI6Kty6mg6jF0000 │ hash: qu4MWboDFlheftTyytXftw type: notebook │ branch: main space: all │ created_at: 2026-08-07 16:20:31 UTC created_by: anonymous └── source_code: │ # %% [markdown] │ # [](https://docs.lamin.ai) [ via … │ # │ # Architecture? │ # │ …
Track a project or an agent plan.
Pass a project/artifact to ln.track(), for example:
Note that you have to create a project or save the agent plan in case they don’t yet exist:
# create a project with the CLI
lamin create project "My project"
# save an agent plan with the CLI
lamin save /path/to/.cursor/plans/curate-dataset-x.plan.md
lamin save /path/to/.claude/plans/curate-dataset-x.md
Or in Python:
You can track workflows by decorating functions:
import lamindb as ln
@ln.flow()
def create_fasta(fasta_file: str = "sample.fasta"):
open(fasta_file, "w").write(">seq1\nACGT\n") # create dataset
ln.Artifact(fasta_file, key=fasta_file).save() # save dataset
if __name__ == "__main__":
pass
Beyond what you get for scripts & notebooks, this automatically tracks function & CLI params and integrates well with established Python workflow managers: docs.lamin.ai/track. To integrate advanced bioinformatics pipeline managers like Nextflow, see docs.lamin.ai/pipelines.
A richer example.
Here is an automatically generated re-construction of the project of Schmidt et al. (Science, 2022):
A phenotypic CRISPRa screening result is integrated with scRNA-seq data. Here is the result of the screen input:
Label artifacts¶
You can label an artifact by running:
my_label = ln.ULabel(name="My label").save() # a universal label
project = ln.Project(name="My project").save() # a project label
artifact.ulabels.add(my_label)
artifact.projects.add(project)
Query for it:
ln.Artifact.filter(ulabels=my_label, projects=project).to_dataframe()
| uid | key | description | suffix | kind | otype | size | hash | n_files | n_observations | ... | is_latest | is_locked | created_at | branch_id | created_on_id | space_id | storage_id | run_id | schema_id | created_by_id | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| id | |||||||||||||||||||||
| 1 | DJuc2fG1vdkKomLs0000 | sample.fasta | None | .fasta | None | None | 11 | 83rEPcAoBHmYiIuyBYrFKg | None | None | ... | True | False | 2026-08-07 16:20:31.081000+00:00 | 1 | 1 | 1 | 1 | 1 | None | 1 |
1 rows × 22 columns
You can also query by the metadata that lamindb automatically collects:
ln.Artifact.filter(run=run).to_dataframe() # by creating run
ln.Artifact.filter(transform=transform).to_dataframe() # by creating transform
ln.Artifact.filter(size__gt=1e6).to_dataframe() # size greater than 1MB
| uid | id | key | description | suffix | kind | otype | size | hash | n_files | ... | is_latest | is_locked | created_at | branch_id | created_on_id | space_id | storage_id | run_id | schema_id | created_by_id |
|---|
0 rows × 23 columns
If you want to include more information into the resulting dataframe, pass include.
ln.Artifact.to_dataframe(include=["created_by__name", "storage__root"]) # include fields from related registries
| uid | key | created_by__name | storage__root | |
|---|---|---|---|---|
| id | ||||
| 1 | DJuc2fG1vdkKomLs0000 | sample.fasta | None | /home/runner/work/lamindb/lamindb/test-readme |
The query syntax for DB objects and for your default database is the same.
Here is an overview that illustrates how artifacts can be labeled by other entities:
Read more: docs.lamin.ai/organize.
Manage features & records¶
Let’s define some features:
from datetime import date
gc_content = ln.Feature(name="gc_content", dtype=float).save()
experiment_note = ln.Feature(name="experiment_note", dtype=str).save()
experiment_date = ln.Feature(name="experiment_date", dtype=date, coerce=True).save() # accept date strings
The most basic thing you can do with features is annotating artifacts, records, or runs with them:
artifact.features.set_values({
gc_content: 0.55,
experiment_note: "Looks great",
experiment_date: "2025-10-24",
})
# query
ln.Artifact.filter(experiment_date == "2025-10-24").to_dataframe(include="features") # query all artifacts annotated with `experiment_date`
| uid | key | gc_content | experiment_note | experiment_date | |
|---|---|---|---|---|---|
| id | |||||
| 1 | DJuc2fG1vdkKomLs0000 | sample.fasta | 0.55 | Looks great | 2025-10-24 |
You can create records for entities underlying your experiments (samples, perturbations, instruments, etc.):
ln.Record(name="Sample 1", features={gc_content: 0.5}).save()
Record(uid='gMu7Q5a4YhGvmXJ6', is_type=False, name='Sample 1', description=None, reference=None, reference_type=None, extra_data=None, branch_id=1, created_on_id=1, space_id=1, created_by_id=1, type_id=None, schema_id=None, run_id=None, created_at=2026-08-07 16:20:34 UTC, is_locked=False)
You can dynamically create registries and relationships of entities via record types:
# create an experiments registry by defining a record type
experiments_registry = ln.Record(name="Experiments", is_type=True).save()
# create a record inside the Experiments registry
ln.Record(name="Experiment 1", type=experiments_registry).save()
# create a feature that links experiments, creating a relationship
experiment = ln.Feature(name="experiment", dtype=experiments_registry).save()
# create a sample record that links the sample to `Experiment 1` via the `experiment` feature
ln.Record(name="Sample 2", features={gc_content: 0.5, experiment: "Experiment 1"}).save()
# export a registry to a dataframe
experiments_registry.to_dataframe()
! you are trying to create a record with name='experiment' but records with similar names exist: 'experiment_note', 'experiment_date'. Did you mean to load one of them?
! you are trying to create a record with name='Sample 2' but a record with similar name exists: 'Sample 1'. Did you mean to load it?
→ exporting 1 records of 'Experiments'
| __lamindb_record_uid__ | __lamindb_record_name__ | |
|---|---|---|
| __lamindb_record_id__ | ||
| 3 | 7rGAKLGBMm0ecqLJ | Experiment 1 |
You can edit records like Excel sheets on LaminHub.
Lakehouse¶
Here is how you ingest a DataFrame:
import pandas as pd
df = pd.DataFrame({
"sequence_str": ["ACGT", "TGCA"],
"gc_content": [0.55, 0.54],
"experiment_note": ["Looks great", "Ok"],
"experiment_date": [date(2025, 10, 24), date(2025, 10, 25)],
})
ln.Artifact.from_dataframe(df, key="my_datasets/sequences.parquet").save() # no validation
Artifact(uid='Wabmyf4yafKacMA60000', key='my_datasets/sequences.parquet', description=None, suffix='.parquet', kind='dataset', otype='DataFrame', size=3405, hash='iK-5bby-Qk0lbWYf9aL4WQ', n_files=None, n_observations=2, extra_data=None, branch_id=1, created_on_id=1, space_id=1, storage_id=1, run_id=None, schema_id=None, created_by_id=1, created_at=2026-08-07 16:20:35 UTC, is_locked=False, version_tag=None, is_latest=True)
To validate & annotate the content of the dataframe, use the built-in schema valid_features:
ln.Feature(name="sequence_str", dtype=str).save() # define a remaining feature
artifact = ln.Artifact.from_dataframe(
df,
key="my_datasets/sequences.parquet",
schema="valid_features" # validate columns against features
).save()
artifact.describe()
→ returning artifact with same hash: Artifact(uid='Wabmyf4yafKacMA60000', key='my_datasets/sequences.parquet', description=None, suffix='.parquet', kind='dataset', otype='DataFrame', size=3405, hash='iK-5bby-Qk0lbWYf9aL4WQ', n_files=None, n_observations=2, extra_data=None, branch_id=1, created_on_id=1, space_id=1, storage_id=1, run_id=None, schema_id=None, created_by_id=1, created_at=2026-08-07 16:20:35 UTC, is_locked=False, version_tag=None, is_latest=True); to track this artifact as an input, use: ln.Artifact.get()
→ loading artifact into memory for validation
Artifact: my_datasets/sequences.parquet (0000) ├── uid: Wabmyf4yafKacMA60000 kind: dataset │ otype: DataFrame hash: iK-5bby-Qk0lbWYf9aL4WQ │ size: 3.3 KB branch: main │ space: all created_at: 2026-08-07 16:20:35 UTC │ created_by: anonymous n_observations: 2 │ schema: valid_features ├── storage/path: /home/runner/work/lamindb/lamindb/test-readme/.lamindb/Wabmyf4yafKacMA60000.parquet └── Dataset features └── columns (4) experiment_date date experiment_note str gc_content float sequence_str str
Watch a mini video: youtu.be/Ji6E7hTnReQ
You can filter for datasets by schema and then launch distributed queries or batch load distributed datasets. For tables, see: docs.lamin.ai/tables. For arrays, see: docs.lamin.ai/arrays.
To validate an AnnData, call:
import anndata as ad
import numpy as np
import pandas as pd
adata = ad.AnnData(
X=np.ones((21, 10)),
obs=pd.DataFrame({'cell_type_by_model': ['T cell', 'B cell', 'NK cell'] * 7}),
var=pd.DataFrame(index=[f'ENSG{i:011d}' for i in range(10)])
)
artifact = ln.Artifact.from_anndata(
adata,
key="my_datasets/scrna.h5ad",
schema="ensembl_gene_ids_and_valid_features_in_obs"
).save()
artifact.describe()
→ loading artifact into memory for validation
/opt/hostedtoolcache/Python/3.14.6/x64/lib/python3.14/functools.py:982: ImplicitModificationWarning: Transforming to str index.
return dispatch(args[0].__class__)(*args, **kw)
✓ created 1 Organism record from Bionty matching name: 'human'
! no values were validated for columns!
✓ added 2 records from_public with bionty.Gene for "columns": 'ENSG00000000003', 'ENSG00000000005'
→ returning schema with same hash: Schema(uid='0000000000000000', is_type=False, name='valid_features', description=None, n_members=None, coerce=None, flexible=True, itype='Feature', otype=None, suffix=None, hash='kMi7B_N88uu-YnbTLDU-DA', minimal_set=True, ordered_set=False, maximal_set=False, branch_id=1, created_on_id=1, space_id=1, created_by_id=1, run_id=None, type_id=None, created_at=2026-08-07 16:20:35 UTC, is_locked=False)
Artifact: my_datasets/scrna.h5ad (0000) ├── uid: ytiGCYvblkImH8Hk0000 kind: dataset │ otype: AnnData hash: uBhn8P4HVyl77x1zR9ju5w │ size: 20.8 KB branch: main │ space: all created_at: 2026-08-07 16:20:37 UTC │ created_by: anonymous n_observations: 21 │ schema: anndata_ensembl_gene_ids_and_valid_features_in_obs ├── storage/path: /home/runner/work/lamindb/lamindb/test-readme/.lamindb/ytiGCYvblkImH8Hk0000.h5ad └── Dataset features ├── obs (None) └── var.T (2 bionty.Gene.ensembl… TNMD num TSPAN6 num
To validate a SpatialData or any other array-like dataset, you need to construct a Schema. You can do this by composing simple pandera-style schemas: docs.lamin.ai/curate.
Branching & versioning¶
LaminDB co-versions code and datasets for you.
If edit and run the create_fasta.py script, you’ll automatically create a new version of the transform and the sample.fasta artifact.
The edited script
# create_fasta.py
import lamindb as ln
ln.track()
open("sample.fasta", "w").write(">seq1\nTGCA\n") # a new sequence
ln.Artifact("sample.fasta", key="sample.fasta", features={"experiment": "Experiment 1"}).save() # annotate with the new experiment
ln.finish()
→ found notebook README.ipynb, making new version -- anticipating changes
→ created Transform('iI6Kty6mg6jF0001', key='README.ipynb'), started new Run('c4GuPp1RIJJYtPjl') at 2026-08-07 16:20:37 UTC
→ notebook imports: anndata==0.13.2 bionty==2.4.3 lamindb numpy==2.5.1 pandas==2.3.3
• tip: to identify the notebook across renames, pass the uid: ln.track("iI6Kty6mg6jF")
→ creating new artifact version for key 'sample.fasta' in storage '/home/runner/work/lamindb/lamindb/test-readme'
! cells [(4, 6), (8, 11), (22, 24)] were not run consecutively
/opt/hostedtoolcache/Python/3.14.6/x64/lib/python3.14/site-packages/nbformat/_struct.py:28: RuntimeWarning: coroutine '_wait_for_close' was never awaited
def __init__(self, *args, **kw):
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
→ returning artifact with same hash: Artifact(uid='0lkqP4AoQAnjWy3D0000', key=None, description='Report of run RXwpzbmf2n1Ylbdk', suffix='.html', kind='__lamindb_run__', otype=None, size=344568, hash='JUpgwy08L66BVW1OB-bKHg', n_files=None, n_observations=None, extra_data=None, branch_id=1, created_on_id=1, space_id=1, storage_id=1, run_id=None, schema_id=None, created_by_id=1, created_at=2026-08-07 16:20:34 UTC, is_locked=False, version_tag=None, is_latest=True); to track this artifact as an input, use: ln.Artifact.get()
! run was not set on Artifact(uid='0lkqP4AoQAnjWy3D0000', key=None, description='Report of run RXwpzbmf2n1Ylbdk', suffix='.html', kind='__lamindb_run__', otype=None, size=344568, hash='JUpgwy08L66BVW1OB-bKHg', n_files=None, n_observations=None, extra_data=None, branch_id=1, created_on_id=1, space_id=1, storage_id=1, run_id=None, schema_id=None, created_by_id=1, created_at=2026-08-07 16:20:34 UTC, is_locked=False, version_tag=None, is_latest=True), setting to current run
! updated description from Report of run RXwpzbmf2n1Ylbdk to Report of run c4GuPp1RIJJYtPjl
! returning transform with same hash & key: Transform(uid='iI6Kty6mg6jF0000', key='README.ipynb', description='LaminDB: data management for traceable, multimodal AI', kind='notebook', hash='qu4MWboDFlheftTyytXftw', reference=None, reference_type=None, environment=None, plan=None, branch_id=1, created_on_id=1, space_id=1, run_id=None, created_by_id=1, created_at=2026-08-07 16:20:31 UTC, is_locked=False, version_tag=None, is_latest=False)
! run was not set on Transform(uid='iI6Kty6mg6jF0000', key='README.ipynb', description='LaminDB: data management for traceable, multimodal AI', kind='notebook', hash='qu4MWboDFlheftTyytXftw', reference=None, reference_type=None, environment=None, plan=None, branch_id=1, created_on_id=1, space_id=1, run_id=None, created_by_id=1, created_at=2026-08-07 16:20:31 UTC, is_locked=False, version_tag=None, is_latest=False), setting to current run
• new latest Transform version is: iI6Kty6mg6jF0000
→ finished Run('c4GuPp1RIJJYtPjl') after 1s at 2026-08-07 16:20:38 UTC
artifact_latest = ln.Artifact.get(key="sample.fasta") # pass version for a previous version: ln.Artifact.get(key="sample.fasta", version="1.0")
artifact_latest.versions.to_dataframe() # all versions of that artifact
artifact_latest.transform.versions.to_dataframe() # all versions of the transform that created the artifact
| uid | key | description | kind | source_code | hash | reference | reference_type | version_tag | is_latest | is_locked | created_at | branch_id | created_on_id | space_id | environment_id | plan_id | run_id | created_by_id | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| id | |||||||||||||||||||
| 1 | iI6Kty6mg6jF0000 | README.ipynb | LaminDB: data management for traceable, multim... | notebook | # %% [markdown]\n# [
artifact = db.Artifact.get(key="example_datasets/mini_immuno/dataset1.h5ad")
artifact.save()
! database has module pertdb, configure it: lamin settings modules set bionty,pertdb
/opt/hostedtoolcache/Python/3.14.6/x64/lib/python3.14/site-packages/fsspec/asyn.py:108: RuntimeWarning: coroutine '_wait_for_close' was never awaited
def sync_wrapper(func, obj=None):
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
→ mapped: Schema(uid='0000000000000002')
→ transferred: Artifact(uid='9K1dteZ6Qx0EXK8g0000'), Storage(uid='D9BilDV2')
Artifact(uid='9K1dteZ6Qx0EXK8g0000', key='example_datasets/mini_immuno/dataset1.h5ad', description='Flow cytometry readouts on invitro cell culture', suffix='.h5ad', kind='dataset', otype='AnnData', size=31672.0, hash='FB3CeMjmg1ivN6HDy6wsSg', n_files=None, n_observations=3.0, extra_data=None, branch_id=1, created_on_id=1, space_id=1, storage_id=2, run_id=3, schema_id=4, created_by_id=1, created_at=2025-07-29 12:27:25 UTC, is_locked=False, version_tag=None, is_latest=True)
This is zero-copy for the artifact’s data in storage. Read more: docs.lamin.ai/transfer.
Ontologies¶
Plugin bionty gives you >20 public ontologies as SQLRecord registries. This was used to validate the ENSG ids in the adata just before.
import bionty as bt
bt.CellType.import_source() # import the default ontology
bt.CellType.to_dataframe() # your extensible cell type ontology in a simple registry
✓ import is completed!
! truncated query result to limit=20 CellType objects
| uid | name | ontology_id | abbr | synonyms | description | is_locked | created_at | branch_id | created_on_id | space_id | created_by_id | run_id | source_id | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| id | ||||||||||||||
| 3437 | 1ChUsEzDZXWW4B | beam B cell, human | CL:7770006 | None | None | A Trabecular Meshwork Cell Within The Eye'S Tr... | False | 2026-08-07 16:20:41.804000+00:00 | 1 | 1 | 1 | 1 | None | 26 |
| 3436 | 5xoxfxIf7WrLdU | beam cell | CL:7770005 | None | None | A Trabecular Meshwork Cell That Is Part Of The... | False | 2026-08-07 16:20:41.804000+00:00 | 1 | 1 | 1 | 1 | None | 26 |
| 3435 | 2j5mhhFoV2vBDV | suprabasal cell | CL:7770004 | None | None | An Epithelial Cell That Resides In The Layer(S... | False | 2026-08-07 16:20:41.804000+00:00 | 1 | 1 | 1 | 1 | None | 26 |
| 3434 | RBCFqAmkM1oaaZ | beam A cell | CL:7770003 | None | None | A Beam Cell Within The Eye'S Trabecular Meshwo... | False | 2026-08-07 16:20:41.804000+00:00 | 1 | 1 | 1 | 1 | None | 26 |
| 3433 | 79Ow7BGPRP018I | juxtacanalicular tissue cell | CL:7770002 | None | None | A Trabecular Meshwork Cell Of The Juxtacanalic... | False | 2026-08-07 16:20:41.804000+00:00 | 1 | 1 | 1 | 1 | None | 26 |
| 3432 | 4qJMS0d5FyIXQK | OB FRMD7 GABA GABAergic neuron (Primate) | CL:4310148 | None | OB FRMD7 GABA | A Gabaergic Neuron Of The Primates Brain. Thes... | False | 2026-08-07 16:20:41.804000+00:00 | 1 | 1 | 1 | 1 | None | 26 |
| 3431 | 6iU1Q2FIIkrgND | OB Dopa-GABA neuron (Primate) | CL:4310147 | None | OB Dopa-GABA | A Ob-Dopa-Gaba Of The Primates Brain. These Ce... | False | 2026-08-07 16:20:41.804000+00:00 | 1 | 1 | 1 | 1 | None | 26 |
| 3430 | 100qUn3IH1Ksw2 | AMY-SLEA-BNST GABA GABAergic interneuron (Prim... | CL:4310146 | None | AMY-SLEA-BNST GABA | A Gabaergic Interneuron Of The Primates Brain.... | False | 2026-08-07 16:20:41.804000+00:00 | 1 | 1 | 1 | 1 | None | 26 |
| 3429 | QJ8929f49Ncfrf | AMY-SLEA-BNST D1 GABA GABAergic interneuron (P... | CL:4310145 | None | AMY-SLEA-BNST D1 GABA | A Gabaergic Interneuron Of The Primates Brain.... | False | 2026-08-07 16:20:41.804000+00:00 | 1 | 1 | 1 | 1 | None | 26 |
| 3428 | 2RruqlADchF4D3 | VTR-HTH Glut glutamatergic neuron of the basal... | CL:4310144 | None | F M Glut|VTR-HTH Glut | A Glutamatergic Neuron Of The Basal Ganglia Of... | False | 2026-08-07 16:20:41.804000+00:00 | 1 | 1 | 1 | 1 | None | 26 |
| 3427 | 44qrwytpXARNMz | ZI-HTH GABA GABAergic interneuron (Primate) | CL:4310143 | None | ZI-HTH GABA | A Gabaergic Interneuron Of The Primates Brain.... | False | 2026-08-07 16:20:41.804000+00:00 | 1 | 1 | 1 | 1 | None | 26 |
| 3426 | 3dVBqPp88IUIib | STRv D2 medium spiny neuron (Primate) | CL:4310138 | None | D2-Shell/OT|STRv D2 MSN | A Nucleus Accumbens Shell And Olfactory Tuberc... | False | 2026-08-07 16:20:41.804000+00:00 | 1 | 1 | 1 | 1 | None | 26 |
| 3425 | 4RRtGKe1oGoVGV | STRv D1-NUDAP medium spiny neuron (Primate) | CL:4310137 | None | D1-NUDAP|STRv D1 NUDAP MSN | A D1-Nudap Medium Spiny Neuron Of The Primates... | False | 2026-08-07 16:20:41.804000+00:00 | 1 | 1 | 1 | 1 | None | 26 |
| 3424 | 1Tw4GET7C0W2Az | STRv D1 medium spiny neuron (Primate) | CL:4310136 | None | STRv D1 MSN|D1-Shell/OT | A Nucleus Accumbens Shell And Olfactory Tuberc... | False | 2026-08-07 16:20:41.804000+00:00 | 1 | 1 | 1 | 1 | None | 26 |
| 3423 | 3taUBSHOL3ffx3 | STR cholinergic-GABAergic neuron (Primate) | CL:4310135 | None | STR Cholinergic GABA | A Striatal Cholinergic-Gabaergic Neuron Of The... | False | 2026-08-07 16:20:41.804000+00:00 | 1 | 1 | 1 | 1 | None | 26 |
| 3422 | 2OzOl5s6HeWYYM | striosomal D2 medium spiny neuron (Primate) | CL:4310134 | None | D2-Striosome|STRd D2 Striosome MSN | A Striosomal D2 Medium Spiny Neuron Of The Pri... | False | 2026-08-07 16:20:41.804000+00:00 | 1 | 1 | 1 | 1 | None | 26 |
| 3421 | 3ZypczMtxf4KQu | STRd D2 Striomat hybrid medium spiny neuron (P... | CL:4310133 | None | STRd D2 StrioMat Hybrid MSN | A Indirect Pathway Medium Spiny Neuron Of The ... | False | 2026-08-07 16:20:41.804000+00:00 | 1 | 1 | 1 | 1 | None | 26 |
| 3420 | 2Mk5r9Xsi24yLc | STRd D2 Matrix medium spiny neuron (Primate) | CL:4310132 | None | D2-Matrix|STRd D2 Matrix MSN | A Matrix D2 Medium Spiny Neuron Of The Primate... | False | 2026-08-07 16:20:41.804000+00:00 | 1 | 1 | 1 | 1 | None | 26 |
| 3419 | 5e4cn19DcnuNH0 | STRd D1/D2-hybrid medium spiny neuron (Primate) | CL:4310131 | None | D1/D2 Hybrid|STR D1D2 Hybrid MSN | A D1/D2-Hybrid Medium Spiny Neuron Of The Prim... | False | 2026-08-07 16:20:41.804000+00:00 | 1 | 1 | 1 | 1 | None | 26 |
| 3418 | 67anmsVZv7HmGa | striosomal D1 medium spiny neuron (Primate) | CL:4310130 | None | STRd D1 Striosome MSN|D1-Striosome | A Striosomal D1 Medium Spiny Neuron Of The Pri... | False | 2026-08-07 16:20:41.804000+00:00 | 1 | 1 | 1 | 1 | None | 26 |
You can then create objects, e.g. for labeling, analogous to ULabel, Project, or Record:
t_cell = bt.CellType.get(name="T cell")
artifact.cell_types.add(t_cell)
Read more: docs.lamin.ai/manage-ontologies.
Watch a mini video: youtu.be/3vpWjHj3Kw8
Manage notes¶
When in your development directory, you can save markdown files as records:
lamin save <topic>/<my-note.md>