diff --git a/modules/msk/annotate_hlahd/environment.yml b/modules/msk/annotate_hlahd/environment.yml
new file mode 100644
index 00000000..da913a15
--- /dev/null
+++ b/modules/msk/annotate_hlahd/environment.yml
@@ -0,0 +1,7 @@
+---
+# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json
+channels:
+ - conda-forge
+ - bioconda
+dependencies:
+ - "ANNOTATE_HLAHD=HERE"
diff --git a/modules/msk/annotate_hlahd/main.nf b/modules/msk/annotate_hlahd/main.nf
new file mode 100644
index 00000000..1b2c2898
--- /dev/null
+++ b/modules/msk/annotate_hlahd/main.nf
@@ -0,0 +1,50 @@
+process ANNOTATE_HLAHD {
+ tag "$meta.id"
+ label 'process_single'
+
+ conda "${moduleDir}/environment.yml"
+ container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ?
+ 'docker://ghcr.io/mskcc-omics-workflows/hlahd-tools:1.0.0':
+ 'ghcr.io/mskcc-omics-workflows/hlahd-tools:1.0.0' }"
+
+ input:
+ tuple val(meta), path(result_dir)
+ path pgroup_file
+
+ output:
+ tuple val(meta), path("${prefix}_annotated.tsv"), emit: tsv
+ tuple val(meta), path("${prefix}_report.html"), emit: report, optional: true
+ path "versions.yml", emit: versions
+
+ when:
+ task.ext.when == null || task.ext.when
+
+ script:
+ def args = task.ext.args ?: ''
+ prefix = task.ext.prefix ?: "${meta.id}"
+ """
+ annotate_hlahd \\
+ --result_dir ${result_dir} \\
+ --sample ${prefix} \\
+ --pgroup_file ${pgroup_file} \\
+ --outdir . \\
+ ${args}
+
+ cat <<-END_VERSIONS > versions.yml
+ "${task.process}":
+ python: \$(python3 --version | sed 's/Python //')
+ END_VERSIONS
+ """
+
+ stub:
+ prefix = task.ext.prefix ?: "${meta.id}"
+ """
+ echo -e "locus\\tallele1\\tallele2\\tp_group" > ${prefix}_annotated.tsv
+ echo "
stub report" > ${prefix}_report.html
+
+ cat <<-END_VERSIONS > versions.yml
+ "${task.process}":
+ python: 3.11
+ END_VERSIONS
+ """
+}
diff --git a/modules/msk/annotate_hlahd/meta.yml b/modules/msk/annotate_hlahd/meta.yml
new file mode 100644
index 00000000..f0a4f2da
--- /dev/null
+++ b/modules/msk/annotate_hlahd/meta.yml
@@ -0,0 +1,64 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/meta-schema.json
+name: "annotate_hlahd"
+description: Annotate HLA-HD class I output with IMGT P-groups and quality flags
+keywords:
+ - hla
+ - hlahd
+ - annotation
+ - immunogenomics
+tools:
+ - "annotate_hlahd":
+ description: "Post-processes HLA-HD's per-sample final.result.txt/*.est.txt class I output, mapping each allele call to its IMGT P-group and flagging low-confidence calls."
+ homepage: "https://github.com/mskcc/hla-annotate"
+ documentation: "https://github.com/mskcc/hla-annotate"
+ licence:
+ - "MIT"
+ identifier: ""
+input:
+ - - meta:
+ type: map
+ description: |
+ Groovy Map containing sample information
+ e.g. `[ id:'sample1' ]`
+ - result_dir:
+ type: directory
+ description: Directory containing HLA-HD's _final.result.txt and _{A,B,C}.est.txt
+ pattern: "*"
+ - - pgroup_file:
+ type: file
+ description: IMGT wmda/hla_nom_p.txt P-group reference table
+ pattern: "*.txt"
+output:
+ - tsv:
+ - meta:
+ type: map
+ description: |
+ Groovy Map containing sample information
+ e.g. `[ id:'sample1' ]`
+ - ${prefix}_annotated.tsv:
+ type: file
+ description: Per-allele annotated TSV with P-group and quality-flag columns
+ pattern: "*_annotated.tsv"
+ - report:
+ - meta:
+ type: map
+ description: |
+ Groovy Map containing sample information
+ e.g. `[ id:'sample1' ]`
+ - ${prefix}_report.html:
+ type: file
+ description: |
+ Self-contained HTML report for the sample. Optional — pass `--skip_html`
+ via `task.ext.args` to skip generating it.
+ pattern: "*_report.html"
+ - versions:
+ - versions.yml:
+ type: file
+ description: File containing software versions
+ pattern: "versions.yml"
+ ontologies:
+ - edam: http://edamontology.org/format_3750
+authors:
+ - "@johnoooh"
+maintainers:
+ - "@johnoooh"
diff --git a/modules/msk/annotate_hlahd/tests/main.nf.test b/modules/msk/annotate_hlahd/tests/main.nf.test
new file mode 100644
index 00000000..9ef53ac0
--- /dev/null
+++ b/modules/msk/annotate_hlahd/tests/main.nf.test
@@ -0,0 +1,115 @@
+nextflow_process {
+
+ name "Test Process ANNOTATE_HLAHD"
+ script "../main.nf"
+ process "ANNOTATE_HLAHD"
+
+ tag "modules"
+ tag "modules_nfcore"
+ tag "modules_msk"
+ tag "annotate_hlahd"
+
+ // Test 1: full run with real (synthetic) data.
+ // The module's result_dir input expects a single directory containing
+ // _final.result.txt and _{A,B,C}.est.txt; test_data.config
+ // registers those as individual files, so this test stages them into one
+ // directory before invoking the process.
+ test("annotate_hlahd - result_dir - annotated tsv and html") {
+
+ when {
+ process {
+ """
+ def resultDir = File.createTempDir()
+ new File(resultDir, "test_sample_final.result.txt")
+ .bytes = file(params.test_data_mskcc['annotate_hlahd']['final_result_txt'], checkIfExists: true).bytes
+ new File(resultDir, "test_sample_A.est.txt")
+ .bytes = file(params.test_data_mskcc['annotate_hlahd']['est_a_txt'], checkIfExists: true).bytes
+ new File(resultDir, "test_sample_B.est.txt")
+ .bytes = file(params.test_data_mskcc['annotate_hlahd']['est_b_txt'], checkIfExists: true).bytes
+ new File(resultDir, "test_sample_C.est.txt")
+ .bytes = file(params.test_data_mskcc['annotate_hlahd']['est_c_txt'], checkIfExists: true).bytes
+
+ input[0] = [
+ [ id:'test_sample' ],
+ file(resultDir)
+ ]
+ input[1] = file(params.test_data_mskcc['annotate_hlahd']['pgroup_file'], checkIfExists: true)
+ """
+ }
+ }
+
+ then {
+ assertAll(
+ { assert process.success },
+ { assert snapshot(
+ process.out.tsv,
+ process.out.report,
+ process.out.versions
+ ).match() }
+ )
+ }
+ }
+
+ // Test 2: --skip_html should suppress the (optional) report output.
+ test("annotate_hlahd - skip_html - no report emitted") {
+
+ config "./nextflow.config"
+
+ when {
+ process {
+ """
+ def resultDir = File.createTempDir()
+ new File(resultDir, "test_sample_final.result.txt")
+ .bytes = file(params.test_data_mskcc['annotate_hlahd']['final_result_txt'], checkIfExists: true).bytes
+ new File(resultDir, "test_sample_A.est.txt")
+ .bytes = file(params.test_data_mskcc['annotate_hlahd']['est_a_txt'], checkIfExists: true).bytes
+ new File(resultDir, "test_sample_B.est.txt")
+ .bytes = file(params.test_data_mskcc['annotate_hlahd']['est_b_txt'], checkIfExists: true).bytes
+ new File(resultDir, "test_sample_C.est.txt")
+ .bytes = file(params.test_data_mskcc['annotate_hlahd']['est_c_txt'], checkIfExists: true).bytes
+
+ input[0] = [
+ [ id:'test_sample' ],
+ file(resultDir)
+ ]
+ input[1] = file(params.test_data_mskcc['annotate_hlahd']['pgroup_file'], checkIfExists: true)
+ """
+ }
+ }
+
+ then {
+ assertAll(
+ { assert process.success },
+ { assert process.out.report == [] },
+ { assert snapshot(process.out.tsv, process.out.versions).match() }
+ )
+ }
+ }
+
+ // Test 3: stub run (no container needed)
+ test("annotate_hlahd - stub") {
+
+ options "-stub"
+
+ when {
+ process {
+ """
+ input[0] = [
+ [ id:'test_sample' ],
+ file('result_dir')
+ ]
+ input[1] = file('hla_nom_p.txt')
+ """
+ }
+ }
+
+ then {
+ assertAll(
+ { assert process.success },
+ { assert path(process.out.tsv.get(0).get(1)).exists() },
+ { assert path(process.out.report.get(0).get(1)).exists() },
+ { assert snapshot(process.out.versions).match() }
+ )
+ }
+ }
+}
diff --git a/modules/msk/annotate_hlahd/tests/main.nf.test.snap b/modules/msk/annotate_hlahd/tests/main.nf.test.snap
new file mode 100644
index 00000000..e734df81
--- /dev/null
+++ b/modules/msk/annotate_hlahd/tests/main.nf.test.snap
@@ -0,0 +1,62 @@
+{
+ "annotate_hlahd - skip_html - no report emitted": {
+ "content": [
+ [
+ [
+ {
+ "id": "test_sample"
+ },
+ "test_sample_annotated.tsv:md5,a8636bc09b81afefdd01e388317b1978"
+ ]
+ ],
+ [
+ "versions.yml:md5,7303efd9ae753a785695afaae7bca831"
+ ]
+ ],
+ "timestamp": "2026-08-26T17:30:52.702765",
+ "meta": {
+ "nf-test": "0.9.5",
+ "nextflow": "26.04.6"
+ }
+ },
+ "annotate_hlahd - stub": {
+ "content": [
+ [
+ "versions.yml:md5,e1c7ca2bd9524d224402cef26a5fca25"
+ ]
+ ],
+ "timestamp": "2026-08-26T17:30:57.636612",
+ "meta": {
+ "nf-test": "0.9.5",
+ "nextflow": "26.04.6"
+ }
+ },
+ "annotate_hlahd - result_dir - annotated tsv and html": {
+ "content": [
+ [
+ [
+ {
+ "id": "test_sample"
+ },
+ "test_sample_annotated.tsv:md5,a8636bc09b81afefdd01e388317b1978"
+ ]
+ ],
+ [
+ [
+ {
+ "id": "test_sample"
+ },
+ "test_sample_report.html:md5,c78df8f69cf678d42b6aa6fad40d223a"
+ ]
+ ],
+ [
+ "versions.yml:md5,7303efd9ae753a785695afaae7bca831"
+ ]
+ ],
+ "timestamp": "2026-08-26T17:30:44.124301",
+ "meta": {
+ "nf-test": "0.9.5",
+ "nextflow": "26.04.6"
+ }
+ }
+}
\ No newline at end of file
diff --git a/modules/msk/annotate_hlahd/tests/nextflow.config b/modules/msk/annotate_hlahd/tests/nextflow.config
new file mode 100644
index 00000000..52cfe423
--- /dev/null
+++ b/modules/msk/annotate_hlahd/tests/nextflow.config
@@ -0,0 +1,5 @@
+process {
+ withName: 'ANNOTATE_HLAHD' {
+ ext.args = '--skip_html'
+ }
+}
diff --git a/modules/msk/annotate_hlahd/tests/tags.yml b/modules/msk/annotate_hlahd/tests/tags.yml
new file mode 100644
index 00000000..18559792
--- /dev/null
+++ b/modules/msk/annotate_hlahd/tests/tags.yml
@@ -0,0 +1,2 @@
+annotate_hlahd:
+ - "modules/msk/annotate_hlahd/**"
diff --git a/tests/config/test_data.config b/tests/config/test_data.config
index 347b83d6..f4786b2d 100644
--- a/tests/config/test_data.config
+++ b/tests/config/test_data.config
@@ -816,5 +816,13 @@ params {
test_chr22_collapsed_grouped_bam = "${params.test_data_base_msk}/feature/fgbio_collectduplexseqmetrics/testdata/chr22_collapsed_grouped.bam"
test_chr22_collapsed_grouped_bam_bai = "${params.test_data_base_msk}/feature/fgbio_collectduplexseqmetrics/testdata/chr22_collapsed_grouped.bam.bai"
}
+ // NOTE: data lives on the feature/annotate_hlahd branch pending Review Team promotion to an official 'annotate_hlahd' branch.
+ 'annotate_hlahd' {
+ final_result_txt = "${params.test_data_base_msk}/feature/annotate_hlahd/annotate_hlahd/test_sample_final.result.txt"
+ est_a_txt = "${params.test_data_base_msk}/feature/annotate_hlahd/annotate_hlahd/test_sample_A.est.txt"
+ est_b_txt = "${params.test_data_base_msk}/feature/annotate_hlahd/annotate_hlahd/test_sample_B.est.txt"
+ est_c_txt = "${params.test_data_base_msk}/feature/annotate_hlahd/annotate_hlahd/test_sample_C.est.txt"
+ pgroup_file = "${params.test_data_base_msk}/feature/annotate_hlahd/annotate_hlahd/hla_nom_p_demo.txt"
+ }
}
}