Single highlighted base pair inside a DNA strand, representing a variant being annotated

A VCF file tells you that position 43,113,924 on chromosome 17 is a T instead of a G, and that 42 of 60 reads agree. That is a fact about your sequencing run. It is not yet biology. Annotation is the step that answers the questions you actually care about: does this change a protein, is it in a gene that matters, has anyone else seen it, and does it look harmful?

Ensembl VEP (Variant Effect Predictor) is the standard tool for this. It maps each variant onto transcripts, predicts the consequence, and can attach population frequencies, clinical assertions and pathogenicity scores in a single pass.

Install VEP and, critically, the right cache

VEP is only as good as its annotation data. The cache is a downloaded snapshot of Ensembl transcripts and features, and it must match your genome build exactly. Annotating a GRCh38 VCF with a GRCh37 cache produces plausible-looking output that is entirely wrong — the worst kind of failure.

conda create -n vep -c bioconda -c conda-forge ensembl-vep
conda activate vep

# Check what this VEP version expects
vep --help | grep -i 'cache'

# Download the cache for GRCh38, matching your VEP release
vep_install -a cf -s homo_sapiens -y GRCh38 \
  -c $HOME/.vep --CONVERT

# GRCh37 instead? Same command with GRCh37 in both places:
# vep_install -a cf -s homo_sapiens -y GRCh37 -c $HOME/.vep

Two things to verify before you annotate anything real. First, the cache release number printed by vep_install should match what vep expects. Second, the contig naming must match your VCF: Ensembl caches use 1, 2; some pipelines emit chr1, chr2. A mismatch yields zero annotations and no error.

# Does your VCF use chr prefixes?
grep -v '^#' variants.vcf | head -3 | cut -f1
# 1     -> matches Ensembl cache
# chr1  -> add --chr chr1 equivalents or convert the VCF

A first annotation run

vep \
  --input_file variants.vcf \
  --output_file variants.vep.vcf \
  --vcf \
  --cache \
  --assembly GRCh38 \
  --offline \
  --everything \
  --fork 4
FlagWhy it matters
--vcfKeeps the output as valid VCF, preserving your genotypes
--cache + --offlineNo network calls, so results are stable and fast
--assemblyGuards against using the wrong cache by accident
--fork 4Parallelises across 4 processes; scale to your cores
--everythingEnables all standard plugins and frequency sources

--everything is convenient for exploration and dangerous for production, because it silently changes what your output contains when tools update. For a pipeline, enumerate the specific plugins you need so the output is predictable.

Reading the annotation

bcftools query -f '%CHROM\t%POS\t%REF\t%ALT\t%INFO/CSQ\n' variants.vep.vcf | head -5

The CSQ field is a pipe-delimited list, one entry per transcript, ordered by severity. The header line documents the field order and you should read it rather than guessing:

grep '##INFO=<ID=CSQ' variants.vep.vcf | tr '|' '\n' | head -30

Typical fields include Consequence, SYMBOL, Feature (the transcript ID), HGVSc, HGVSp, CANONICAL, IMPACT, gnomADe_AF and CLIN_SIG. Everything you need to triage is usually in the first entry, which is the most severe consequence across transcripts.

Choosing a transcript set is the real analytical decision

A single variant can have different consequences on different transcripts of the same gene: missense on one, intronic on another. VEP reports all of them, so the question which transcript is the truth is yours to answer, not the tool’s.

FlagBehaviourUse when
--canonicalOne consequence per variant, on the canonical transcriptGene-level summaries, simple reports
--maneUses the MANE Select transcriptClinical and cross-database consistency
--pickPicks one consequence by severity/impact rulesFilters that need exactly one row per variant
--per_geneOne consequence per gene, per variantRare disease gene panels

For anything clinical, --mane is the defensible default: MANE Select transcripts are agreed between Ensembl and RefSeq, so a variant described in a paper maps to the same transcript in your output. For exploratory work, keep all consequences and filter downstream — you cannot recover an annotation you chose not to compute.

vep --input_file variants.vcf --output_file variants.mane.vcf \
  --vcf --cache --assembly GRCh38 --offline \
  --mane --symbol --numbers --biotype \
  --plugin CADD,/refs/whole_genome_SNVs.tsv.gz \
  --fork 4

Adding frequencies and clinical evidence

A rare variant in a gene is a candidate; a variant present in 40 percent of the population is usually not. Population frequency is the single most powerful filter, and both gnomAD and ClinVar are easy to attach.

# gnomAD frequencies (offline, uses the cache's gnomAD data)
vep --input_file variants.vcf --output_file variants.freq.vcf \
  --vcf --cache --assembly GRCh38 --offline \
  --af_gnomade --af_gnomadg --max_af

# ClinVar significance from a local copy
# download: https://ftp.ncbi.nlm.nih.gov/pub/clinvar/vcf_GRCh38/clinvar.vcf.gz
vep --input_file variants.vcf --output_file variants.clin.vcf \
  --vcf --cache --assembly GRCh38 --offline \
  --custom file=/refs/clinvar.vcf.gz,short_name=ClinVar,\
format=vcf,type=exact,coords=0,fields=CLNSIG%CLNDN%CLNREVSTAT

Pay attention to CLNREVSTAT. A ClinVar assertion with one submitter and no review is not equivalent to a practice guideline; treating all CLNSIG=Pathogenic entries as equal is a common and consequential shortcut.

Filtering to variants worth following up

The goal is not to delete data; it is to rank. Keep the full annotated VCF as an archive and produce a filtered view for interpretation.

# Rare, high-impact, coding variants
bcftools view -i 'INFO/CSQ ~ "HIGH|MODERATE"' variants.clin.vcf |
bcftools view -i 'INFO/MAX_AF < 0.01 || INFO/MAX_AF = "."' |
bcftools view -e 'INFO/CSQ ~ "synonymous_variant"' \
  -o candidates.vcf.gz -O z

bcftools index candidates.vcf.gz

Then summarise what you have rather than reading the VCF:

bcftools query -f '%CHROM\t%POS\t%SYMBOL\t%Consequence\t%MAX_AF\t%CLIN_SIG\n' \
  candidates.vcf.gz | sort -t$'\t' -k5,5n | head -20

A table sorted by population frequency, showing gene and consequence, is what actually gets discussed in a lab meeting. Note that filtering on MAX_AF = "." keeps variants with no frequency data — which is where genuinely novel candidates live, and also where most artefactual calls live. Both facts matter.

The mistakes that change your conclusions

Wrong genome build

The single most damaging error in this workflow. Check ##reference in your VCF header and match it to the cache. If you inherited a VCF with no clear build, determine it before annotating, not after.

bcftools view -h variants.vcf | grep -E 'reference|assembly|contig=<ID=(chr)?1'

chr-prefix mismatch

Zero annotations, no error. Either rename contigs or add the appropriate cache alias. Always verify a known variant annotates correctly before running the full file.

Trusting one in-silico predictor

SIFT and PolyPhen scores disagree routinely, and both are trained largely on human disease variants, which biases them for non-model organisms. Report the score, its version, and never let a single predictor decide a classification.

Annotating before filtering on quality

VEP will faithfully annotate your worst calls. Filter on depth, genotype quality and allele balance first, so the effort you spend interpreting is spent on real variants.

What to record

Reproducibility here means four numbers: the VEP version, the cache release, the assembly, and the plugin versions with their data file dates. A result annotated against last year’s ClinVar is not wrong, but a reader must be able to see that it is last year’s.

vep --version
ls -la $HOME/.vep/homo_sapiens/
zcat /refs/clinvar.vcf.gz | grep -m1 fileDate

Interpretation is where annotation becomes an argument, and it is the part tools cannot do for you. If you want to go deeper into reading consequences in context — population genetics, inheritance patterns, and how to report a variant defensibly — Ampersand Academy covers the interpretation side in depth.

Practice

  1. Annotate a small GRCh38 VCF with --everything and inspect the CSQ header to find the frequency fields.
  2. Re-run with --mane and compare how many variants change their reported consequence.
  3. Add ClinVar and find a variant whose classification has low review status.
  4. Produce a candidate table of rare, coding, non-synonymous variants sorted by population frequency.

What does VEP actually add to a VCF?

It adds a CSQ INFO field listing, for each transcript a variant overlaps, the predicted consequence, the affected gene and transcript, coding and protein HGVS names, impact category, and any population frequencies or clinical assertions you enabled. Without annotation a VCF contains coordinates only.

Which transcript should I annotate against?

For clinical or publication work use MANE Select via –mane, because it is agreed between Ensembl and RefSeq and therefore matches what other groups report. For exploratory analysis, keep all transcript consequences and choose later rather than discarding information up front.

Why does VEP output no annotations for my variants?

Nearly always a contig naming mismatch or the wrong cache build. Ensembl caches use bare chromosome names such as 1 while many pipelines emit chr1. Confirm by checking the first column of your VCF and the assembly in your header.

Do I need internet access to run VEP?

No, if you download the cache once and use –offline with –cache. Offline mode is strongly preferred in pipelines: it is faster, deterministic, and immune to Ensembl outages or data changes mid-project.

Is VEP the same as ANNOVAR or SnpEff?

They solve the same problem with different annotation sources and transcript definitions. VEP is well maintained, has excellent MANE and frequency support, and is free for all uses. ANNOVAR requires registration and a licence for commercial use, which is a real consideration for industry work.

How do I know a variant is pathogenic?

Not from VEP alone. VEP reports what databases say and what predictors estimate; pathogenicity classification follows frameworks such as ACMG/AMP that weigh population frequency, computational evidence, functional data and segregation. Treat annotations as evidence to be assessed, not verdicts.

How long does annotating a whole genome take?

Roughly 10 to 40 minutes for a typical 4 to 5 million variant VCF with a local cache and several forks, depending on cores, plugins and disk speed. Whole-genome annotations that call out to remote services can take hours, which is another reason to work offline.

Last updated on · Written by