Install any skill in seconds. Free to start, no credit card required.
Get Started Free →End-to-end spatial transcriptomics workflow for Visium/Xenium data. Covers data loading, preprocessing, spatial analysis, domain detection, and visualization with Squidpy. Use when analyzing spatial transcriptomics data.
.claude/skills/bio-workflows-spatial-pipeline/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-11 | ✗→✓ | ▲ Improved | 92% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 80% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 55% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 69% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 85% | 0% |
<!--
#
#
-->
Complete workflow for analyzing Visium, Xenium, or other spatial transcriptomics data.
Spatial data (Space Ranger output)
|
v
[1. Load Data] ---------> Read Visium/Xenium
|
v
[2. QC & Preprocessing] -> Filter, normalize
|
v
[3. Clustering] --------> Standard scRNA-seq clustering
|
v
[4. Spatial Analysis] --> Neighbors, statistics
|
v
[5. Domain Detection] --> Spatial domains
|
v
[6. Visualization] -----> Spatial plots
|
v
Annotated spatial datapythonimport scanpy as sc import squidpy as sq import numpy as np import matplotlib.pyplot as plt # Load Visium data (Space Ranger output) adata = sq.read.visium('spaceranger_output/') # Or load from specific files adata = sc.read_10x_h5('filtered_feature_bc_matrix.h5') adata.uns['spatial'] = ... # Add spatial info # For Xenium adata = sq.read.xenium('xenium_output/') print(f'Loaded: {adata.n_obs} spots/cells, {adata.n_vars} genes')
python# QC metrics adata.var['mt'] = adata.var_names.str.startswith('MT-') sc.pp.calculate_qc_metrics(adata, qc_vars=['mt'], inplace=True) # Visualize QC fig, axes = plt.subplots(1, 3, figsize=(15, 4)) sc.pl.spatial(adata, color='total_counts', ax=axes[0], show=False) sc.pl.spatial(adata, color='n_genes_by_counts', ax=axes[1], show=False) sc.pl.spatial(adata, color='pct_counts_mt', ax=axes[2], show=False) plt.savefig('qc_spatial.pdf') # Filter sc.pp.filter_cells(adata, min_counts=500) sc.pp.filter_cells(adata, min_genes=200) sc.pp.filter_genes(adata, min_cells=10) adata = adata[adata.obs.pct_counts_mt < 25, :] print(f'After QC: {adata.n_obs} spots/cells')
python# Store raw counts adata.layers['counts'] = adata.X.copy() # Normalize sc.pp.normalize_total(adata, target_sum=1e4) sc.pp.log1p(adata) # HVGs sc.pp.highly_variable_genes(adata, n_top_genes=2000) # PCA and clustering adata.raw = adata adata = adata[:, adata.var.highly_variable] sc.pp.scale(adata, max_value=10) sc.tl.pca(adata, n_comps=50) sc.pp.neighbors(adata, n_neighbors=15, n_pcs=30) sc.tl.umap(adata) sc.tl.leiden(adata, resolution=0.5) # Visualize clusters in space sc.pl.spatial(adata, color='leiden', spot_size=1.5) plt.savefig('clusters_spatial.pdf')
python# Build spatial neighbors graph sq.gr.spatial_neighbors(adata, coord_type='generic', n_neighs=6) # Neighborhood enrichment (which clusters are neighbors) sq.gr.nhood_enrichment(adata, cluster_key='leiden') sq.pl.nhood_enrichment(adata, cluster_key='leiden') plt.savefig('nhood_enrichment.pdf') # Co-occurrence analysis sq.gr.co_occurrence(adata, cluster_key='leiden') sq.pl.co_occurrence(adata, cluster_key='leiden') plt.savefig('co_occurrence.pdf') # Spatially variable genes sq.gr.spatial_autocorr(adata, mode='moran', n_perms=100, n_jobs=4) # Top spatially variable genes svg = adata.uns['moranI'].sort_values('I', ascending=False) top_svg = svg.head(20).index.tolist() print('Top spatially variable genes:', top_svg[:10])
python# Spatial domain detection using clustering with spatial constraints # Option 1: Use spatial neighbors for Leiden clustering sq.gr.spatial_neighbors(adata, coord_type='generic', n_neighs=15) sc.tl.leiden(adata, resolution=0.3, key_added='spatial_domains', adjacency=adata.obsp['spatial_connectivities']) # Visualize domains sc.pl.spatial(adata, color='spatial_domains', spot_size=1.5) plt.savefig('spatial_domains.pdf') # Compare transcriptomic vs spatial clusters sc.pl.spatial(adata, color=['leiden', 'spatial_domains'], ncols=2) plt.savefig('clusters_comparison.pdf')
python# Gene expression in space genes = ['EPCAM', 'VIM', 'PTPRC', 'COL1A1'] sc.pl.spatial(adata, color=genes, ncols=2, spot_size=1.5, cmap='viridis') plt.savefig('marker_genes_spatial.pdf') # Cluster markers in space sc.tl.rank_genes_groups(adata, 'leiden', method='wilcoxon') sc.pl.rank_genes_groups_dotplot(adata, n_genes=5) plt.savefig('cluster_markers.pdf') # Save adata.write('spatial_analyzed.h5ad')
pythonimport scanpy as sc import squidpy as sq import matplotlib.pyplot as plt import os # Configuration data_dir = 'spaceranger_output' output_dir = 'spatial_results' os.makedirs(output_dir, exist_ok=True) os.makedirs(f'{output_dir}/plots', exist_ok=True) # Load print('Loading data...') adata = sq.read.visium(data_dir) print(f'Loaded: {adata.n_obs} spots, {adata.n_vars} genes') # QC print('QC filtering...') adata.var['mt'] = adata.var_names.str.startswith('MT-') sc.pp.calculate_qc_metrics(adata, qc_vars=['mt'], inplace=True) sc.pp.filter_cells(adata, min_counts=500) sc.pp.filter_genes(adata, min_cells=10) adata = adata[adata.obs.pct_counts_mt < 25, :] print(f'After QC: {adata.n_obs} spots') # Normalize and cluster print('Processing...') adata.layers['counts'] = adata.X.copy() sc.pp.normalize_total(adata, target_sum=1e4) sc.pp.log1p(adata) sc.pp.highly_variable_genes(adata, n_top_genes=2000) adata.raw = adata adata = adata[:, adata.var.highly_variable] sc.pp.scale(adata, max_value=10) sc.tl.pca(adata, n_comps=50) sc.pp.neighbors(adata, n_neighbors=15, n_pcs=30) sc.tl.leiden(adata, resolution=0.5) # Spatial analysis print('Spatial analysis...') sq.gr.spatial_neighbors(adata, coord_type='generic', n_neighs=6) sq.gr.nhood_enrichment(adata, cluster_key='leiden') sq.gr.spatial_autocorr(adata, mode='moran', n_perms=100) # Plots print('Creating plots...') sc.pl.spatial(adata, color='leiden', spot_size=1.5, save='_clusters.pdf') sq.pl.nhood_enrichment(adata, cluster_key='leiden', save='_nhood.pdf') # Save adata.write(f'{output_dir}/spatial_analyzed.h5ad') print(f'Results saved to {output_dir}/')
<!-- AUTHOR_SIGNATURE: 9a7f3c2e-MD-BABU-MIA-2026-MSSM-SECURE -->
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 24,173 | 16,856 | -30% | 1 | 1 | 0% | 5,528 | 6,134 | +11% | 0 | 0 | — |
case-02 | fail→fail | 16,480 | 13,936 | -15% | 1 | 1 | 0% | 2,934 | 5,192 | +77% | 0 | 0 | — |
case-11 | fail→pass | 11,508 | 6,987 | -39% | 1 | 1 | 0% | 1,901 | 3,644 | +92% | 0 | 0 | — |
case-12 | pass→pass | 11,354 | 4,864 | -57% | 1 | 1 | 0% | 1,885 | 3,071 | +63% | 0 | 0 | — |
case-22 | pass→pass | 16,919 | 14,347 | -15% | 1 | 1 | 0% | 3,197 | 5,276 | +65% | 0 | 0 | — |
case-03 | fail→pass | 9,708 | 5,575 | -43% | 1 | 1 | 0% | 1,779 | 3,204 | +80% | 0 | 0 | — |
case-04 | pass→pass | 7,566 | 3,618 | -52% | 1 | 1 | 0% | 1,522 | 2,809 | +85% | 0 | 0 | — |
case-05 | pass→pass | 13,256 | 9,636 | -27% | 1 | 1 | 0% | 2,612 | 4,146 | +59% | 0 | 0 | — |
case-06 | fail→pass | 12,122 | 6,816 | -44% | 1 | 1 | 0% | 2,202 | 3,415 | +55% | 0 | 0 | — |
case-07 | fail→fail | 14,463 | 8,299 | -43% | 1 | 1 | 0% | 2,517 | 3,796 | +51% | 0 | 0 | — |
case-08 | pass→pass | 6,667 | 4,363 | -35% | 1 | 1 | 0% | 1,252 | 3,005 | +140% | 0 | 0 | — |
case-09 | fail→pass | 11,318 | 6,119 | -46% | 1 | 1 | 0% | 1,994 | 3,377 | +69% | 0 | 0 | — |
case-10 | pass→pass | 11,697 | 7,735 | -34% | 1 | 1 | 0% | 2,172 | 3,718 | +71% | 0 | 0 | — |
case-13 | pass→pass | 13,257 | 6,630 | -50% | 1 | 1 | 0% | 2,555 | 3,534 | +38% | 0 | 0 | — |
case-14 | fail→pass | 9,796 | 5,796 | -41% | 1 | 1 | 0% | 1,801 | 3,333 | +85% | 0 | 0 | — |
case-15 | pass→pass | 11,157 | 7,609 | -32% | 1 | 1 | 0% | 2,266 | 3,568 | +57% | 0 | 0 | — |
case-16 | fail→pass | 9,630 | 5,468 | -43% | 1 | 1 | 0% | 1,878 | 3,298 | +76% | 0 | 0 | — |
case-17 | pass→pass | 6,487 | 3,426 | -47% | 1 | 1 | 0% | 1,188 | 2,954 | +149% | 0 | 0 | — |
case-18 | fail→fail | 11,756 | 7,748 | -34% | 1 | 1 | 0% | 2,145 | 3,711 | +73% | 0 | 0 | — |
case-19 | pass→pass | 7,072 | 2,969 | -58% | 1 | 1 | 0% | 1,392 | 2,813 | +102% | 0 | 0 | — |
case-20 | pass→pass | 11,740 | 10,385 | -12% | 1 | 1 | 0% | 2,289 | 4,287 | +87% | 0 | 0 | — |
case-21 | pass→pass | 15,195 | 13,834 | -9% | 1 | 1 | 0% | 3,026 | 5,141 | +70% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +27 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 7/24/2026 | +27% |
Other measured skills in the registry, with their headline benchmark lift.