Conversion · Seurat object (RDS) → Loom
How to Convert Seurat object to Loom (Without Losing Your Metadata)
SaveLoom writes one assay and can quietly hand you scaled data instead of raw counts, so check the matrix before you trust it in pySCENIC or velocyto.
By Ming "Tommy" Tang, Director of Bioinformatics in Big Pharma · Reviewed September 2026 · 2 min read
- Seurat object (RDS)
- .rds, .RDS · coordinates: n/a
- Loom
- .loom · coordinates: n/a
You need this conversion when a downstream tool speaks loom instead of R: pySCENIC's aucell step, velocyto-based RNA velocity, or a Python collaborator who wants to load your data in scanpy without going through anndata conversion twice. as.loom()/SaveLoom() from SeuratDisk is the standard route in, and it's a one-assay, one-matrix trip, not a full object dump.
What gets lost or reshaped: SaveLoom exports only the assay set as DefaultAssay(), so a multi-assay object (RNA + ADT + SCT) loses everything but one on the way out. Inside that assay, the separate counts, data, and scale.data slots collapse into a single loom matrix. Reductions like PCA and UMAP are optional and only survive if SaveLoom successfully maps them into col_attrs/row_attrs. Metadata column names with periods get rewritten to underscores, silently, which can collide with an existing column of the same underscore name.
The most common way this breaks quietly: a documented SeuratDisk regression (GitHub issue #58) where the exported loom matrix ends up holding normalized or scaled values instead of raw counts, with no error or warning at any step. pySCENIC and velocyto both do math that assumes raw counts. If you skip checking the matrix values before feeding it downstream, you get results that look plausible and are wrong.
The commands
Type your file names once; every command below updates.
01SeuratDisk
rlibrary(SeuratDisk) seurat_obj <- readRDS("sample.rds") SaveLoom(seurat_obj, filename = "sample.loom", overwrite = TRUE)SaveLoom() writes only the active assay (DefaultAssay(seurat_obj)) into the loom's main matrix, converts any dot-containing meta.data column names to underscores, and drops reductions it can't map. It assumes you've already checked which assay and which slot (counts/data/scale.data) you actually want exported, since it collapses all three into one matrix.
02srtdisk
rlibrary(srtdisk) seurat_obj <- readRDS("sample.rds") Convert(seurat_obj, assay = "RNA", to = "loom", filename = "sample.loom")Use this instead of SeuratDisk when the source object is Seurat v5 with Assay5 layers, since the original SeuratDisk doesn't understand split counts/data/scale.data layers. Assumes the named assay exists in the object and that you've picked one assay on purpose, not by default.
03base R
rseurat_obj <- readRDS("sample.rds") slotNames(seurat_obj) seurat_obj[["RNA"]]@counts[1:5, 1:5]Run before conversion, not after. This confirms the counts slot is actually populated with raw integer counts before you trust SaveLoom to export it; an empty or already-normalized counts slot here means your loom file will inherit the same problem with no error thrown.
04janitor
rseurat_obj@meta.data <- janitor::clean_names(seurat_obj@meta.data) |> dplyr::na_if("N/A") saveRDS(seurat_obj, "sample.loom")Standardizes metadata column names and missing-value encoding before conversion, so you control the resulting loom col_attrs names instead of letting SaveLoom's automatic dot-to-underscore rewrite create accidental collisions between columns.
Coordinates, strand, names, builds
Neither Seurat RDS nor loom carries genomic coordinates, so there's no 0-based/1-based or chr-prefix question in this conversion. What actually changes: assay selection, layer collapsing, reduction survival, and metadata naming. SaveLoom exports only DefaultAssay(seurat_obj); if the object has RNA, ADT, and SCT assays, only one makes it into the loom file and the rest disappear without a warning. Within that assay, Seurat's separate counts, data, and scale.data slots collapse into a single loom matrix, and a documented SeuratDisk regression (issue #58) means that matrix can end up holding normalized or scaled values instead of raw counts. Reductions like PCA and UMAP are optional passengers, written into col_attrs/row_attrs as <name>_cell_embeddings only when SaveLoom can map them; nothing guarantees every reduction in your object survives. Metadata column names containing periods (e.g. paper.expression.subtype) are silently rewritten to underscores, which can collide with an existing underscore-named column and overwrite it without any error.
Check the output before you trust it
01Matrix dimensions match the source assay
rlibrary(loomR) lfile <- connect(filename = "output.loom", mode = "r") lfile$shape lfile$close_all()Expected Dimensions equal dim(seurat_obj[["RNA"]]) from the source object (genes x cells); a smaller cell count means an assay or subset got dropped.
02Matrix values are raw counts, not normalized data
rlfile <- connect(filename = "output.loom", mode = "r") lfile$matrix[1:5, 1:5]Expected Integer-like values (0, 1, 2, 3...). Decimal values mean SaveLoom exported the `data` or `scale.data` slot instead of counts (the issue #58 regression).
03Metadata columns survived without collisions
rlfile$col.attrs$names()Expected One attribute per original meta.data column (dots rewritten to underscores), with no unexpected duplicates or missing columns compared to colnames(seurat_obj@meta.data).
04Expected reductions made it into col_attrs
rlfile$col.attrs$names()[grepl("embeddings", lfile$col.attrs$names())]Expected Entries like UMAP_cell_embeddings or PCA_cell_embeddings if you expect those reductions to travel with the file; an empty result means they were dropped.
05File reopens cleanly in a fresh session
Expected connect() succeeds without an HDF5 signature error. An error here means the writing session never called $close_all() and the file may be corrupt.
Errors you will see, and what they mean
- SaveLoom does not store raw count matrix nor metadata (SeuratDisk issue #58)
- Cause: In some SeuratDisk/Seurat version combinations, SaveLoom() exports whichever slot happens to be active instead of the counts slot, so the loom matrix ends up holding normalized or scaled values. Fix: Before converting, check that seurat_obj[["RNA"]]@counts holds integers, not decimals. After converting, open the loom matrix and confirm the values are still integers; if they're not, export counts manually with loomR or pin an older/newer SeuratDisk version.
- pySCENIC/aucell fails with an empty or malformed expression matrix
- Cause: pySCENIC expects genes as rows and cells as columns, but loom orientation conventions differ between velocyto's output and SeuratDisk's SaveLoom output. Fix: Check lfile$shape after conversion and confirm gene count and cell count line up with your source object; transpose or re-export if the orientation is reversed.
- HDF5 error: unable to open file (file signature not found) when re-opening the loom
- Cause: The R session that wrote the loom file exited, crashed, or moved on to the next object without closing the file handle, leaving the HDF5 file in an inconsistent state. Fix: Always call $close_all() on the loomR connection (or make sure SaveLoom's internal writer closes cleanly) immediately after writing, before trying to read the file from a new session.
- Metadata column missing or values look mixed after conversion
- Cause: SeuratDisk rewrites periods in column names to underscores, so paper.expression.subtype and an existing paper_expression_subtype column collide into a single loom attribute and one silently overwrites the other. Fix: Run janitor::clean_names() on seurat_obj@meta.data before conversion so you choose the final names, then check lfile$col.attrs$names() for unexpected duplicates.
- UMAP or PCA coordinates are missing from the loom file even though the conversion ran without error
- Cause: SaveLoom only writes reductions it can successfully map into col_attrs/row_attrs; not every reduction stored in the Seurat object is guaranteed to be exported. Fix: Don't rely on the loom file for downstream plotting that needs UMAP coordinates. Export embeddings separately with Embeddings(seurat_obj, 'umap') and carry them alongside the loom file.
- Convert() throws a layer or assay-version error on a Seurat v5 object
- Cause: SeuratDisk does not support Seurat v5's Assay5 structure, which can split counts/data/scale.data across multiple per-sample layers. Fix: Switch to srtdisk for v5-aware conversion, or run JoinLayers() to collapse the object's layers before attempting the original SeuratDisk path.
Questions people ask
- Why does my loom file have normalized data instead of raw counts?
This is a known SeuratDisk regression tracked as issue #58: in some version combinations, SaveLoom() exports whichever slot is active rather than explicitly pulling from the counts slot. Check seurat_obj[["RNA"]]@counts for integer values before converting, then confirm the loom matrix still holds integers after conversion.
- Does SaveLoom export all of my Seurat assays?
No. It exports only the assay set as DefaultAssay(seurat_obj) at conversion time. If your object has RNA, ADT, or SCT assays, decide which one you need in the loom file and set it as the default before calling SaveLoom, because the others are dropped without a warning.
- Can I convert a Seurat v5 object to loom?
The original SeuratDisk doesn't understand Seurat v5's Assay5 layer structure, so conversions on v5 objects can fail or silently misread layers. Use srtdisk instead, which is built for v5-aware, layer-conscious conversion, or run JoinLayers() first if you want to stick with SeuratDisk.
- Will my UMAP or PCA coordinates survive the conversion?
Sometimes. SaveLoom writes reductions it can map into col_attrs/row_attrs as
<name>_cell_embeddings, but there's no guarantee every reduction in your object makes it across. Check lfile$col.attrs$names() after conversion, and if you need UMAP downstream, export Embeddings(seurat_obj, 'umap') separately as a backup.- Why does my loom file fail to open with an HDF5 error after I wrote it?
Loom is an HDF5-based format, and if the R session that wrote it exits or crashes without closing the file handle, the file can end up corrupted or incomplete. Always call $close_all() on the loomR connection right after writing, before trying to reopen the file in a new session.
Related pages
- Convert · How to Convert Loom to Seurat object (Without Losing Your Metadata)
- Convert · How to Convert h5ad to Seurat object (Without Losing Your Metadata)
- Convert · How to Convert Seurat object to h5ad (Without Losing Your Metadata)
- Convert · How to Convert Seurat object to h5Seurat (Without Losing Your Metadata)
- Convert · How to Convert Seurat object to SingleCellExperiment (Without Losing Your Metadata)
- Glossary · Seurat object
- Glossary · CITE-seq
- Glossary · Count matrix
Sources
- SaveLoom: Save a Seurat object to a loom file — Official SeuratDisk documentation for the SaveLoom() function used in the primary conversion command
- GitHub - mojaveazure/seurat-disk: Interfaces for HDF5-based Single Cell File Formats — Source for metadata dot-to-underscore renaming and col_attrs/row_attrs reduction storage behavior
- GitHub - mianaz/srtdisk: Seurat v5 compatible HDF5 converter — Source for the Seurat v5 / Assay5 conversion workaround command
- SaveLoom does not store raw count matrix nor metadata · Issue #58 — Source for the raw-count regression described as the most common silent failure
- Seurat v5 Essential Commands — Source for how counts/data/scale.data slots are structured in Seurat objects