eCTD vs NeeS vs VNeeS: Submission Format Selection
Before a single leaf file is placed into a module, a submission pipeline has to answer a question that looks trivial and is not: which electronic format does this filing use? Get it wrong and the packaging step downstream produces a structure the receiving authority’s gateway does not recognize — a rejection that has nothing to do with the science in the dossier. This guide covers the three formats a Python automation builder actually encounters — eCTD, NeeS, and VNeeS — and a decision helper that picks the correct one from jurisdiction, product type, and procedure metadata instead of a hardcoded assumption. It sits under FDA/EMA Submission Schema Design, part of Core Architecture & Regulatory Mapping for Clinical Trials, and the format it selects is what the submission gateway assembles and transports.
Why naive approaches fail
Most pipelines get built against a single reference market and then break the first time a second region or a veterinary product enters the picture:
- Assuming eCTD everywhere. eCTD is the dominant format for human drug applications to the FDA and for EMA centralized procedures, but treating it as universal ignores that some national and decentralized procedures outside the largest ICH-aligned markets still expect a different packaging convention, and that veterinary products follow an entirely separate track.
- Ignoring the product-type axis. Region alone does not determine format. A veterinary marketing authorization filed in the EU uses a different electronic format from a human one filed through the same authority, because the two product classes are governed by different regulations with different technical annexes.
- Treating NeeS as still current. NeeS (Non-eCTD electronic Submission) was the CTD-structured, folder-and-PDF format used before XML-backbone submissions became mandatory. Regulatory eSubmission roadmaps have progressively withdrawn acceptance of NeeS for new human submissions in favor of eCTD; a pipeline that still defaults to it for a fresh filing is building toward a format the authority no longer accepts.
- Hardcoding the format at intake instead of deriving it. When the format is a literal string set once in a configuration file, a legitimate exception — a legacy renewal still permitted in the old format, or a small non-ICH market with its own rules — has nowhere to go except a manual override that nobody remembers to audit.
Architecture overview
Selection resolves in a short, ordered chain: identify the target jurisdiction, resolve the product type, check for a legacy exception, then map the remaining combination to a format.
The three formats differ enough that conflating them in code is what causes the rejections above:
| Format | Structure | XML backbone | Current status | Typical use |
|---|---|---|---|---|
| eCTD | ICH Common Technical Document tree, Modules 1-5 | Yes — index.xml with lifecycle operators |
Mandatory for FDA human drug applications (NDA/ANDA/BLA) and EMA centralized procedures; the target format most national procedures are migrating to | New and lifecycle submissions for human medicinal products |
| NeeS | CTD-structured folders of PDFs | No | Being withdrawn; most authorities that once accepted it now expect eCTD for new human filings, with narrowing exceptions for legacy sequences already in that format | Historical sequences and a shrinking set of national procedures |
| VNeeS | CTD-structured folders of PDFs, veterinary technical annexes | No | Current, actively required format for veterinary medicinal product submissions in the EU | New and lifecycle submissions for veterinary products |
VNeeS is easy to mistake for a legacy format because it shares NeeS’s non-XML, folder-based structure, but it is not being retired — it is the format veterinary regulation still specifies, distinct from the human eCTD track and governed by its own module set.
Setup and configuration
The decision rules live in a small, versioned configuration rather than scattered conditionals, so a regulatory-affairs reviewer can audit the rule set without reading code. pyyaml loads it; nothing else is required beyond the standard library.
pip install "pyyaml>=6.0"
# format_rules.yaml — reviewed and versioned alongside the code that reads it
rules:
- jurisdiction: FDA
product_type: human
format: ectd
- jurisdiction: EMA_CENTRALIZED
product_type: human
format: ectd
- jurisdiction: EMA_NATIONAL
product_type: human
format: ectd
legacy_format: nees
- jurisdiction: EMA_CENTRALIZED
product_type: veterinary
format: vnees
- jurisdiction: EMA_NATIONAL
product_type: veterinary
format: vnees
Configuration is loaded from a path in the environment so the same code runs against a reviewed production rule set or a sandbox variant:
"""Environment-driven configuration for format selection."""
from __future__ import annotations
import os
from dataclasses import dataclass
@dataclass(frozen=True)
class SelectorConfig:
rules_path: str
@classmethod
def from_env(cls) -> "SelectorConfig":
value = os.environ.get("FORMAT_RULES_PATH")
if not value:
raise RuntimeError("Missing required environment variable: FORMAT_RULES_PATH")
return cls(rules_path=value)
Full working implementation
The core model is three enums and a context object; the engine walks the loaded rules and returns a typed result rather than a bare string, so a caller cannot forget to check for the legacy branch.
"""Submission format selection for eCTD, NeeS, and VNeeS."""
from __future__ import annotations
import json
import logging
from dataclasses import dataclass
from enum import Enum
import yaml
logger = logging.getLogger("submission.format_selector")
class Jurisdiction(str, Enum):
FDA = "FDA"
EMA_CENTRALIZED = "EMA_CENTRALIZED"
EMA_NATIONAL = "EMA_NATIONAL"
class ProductType(str, Enum):
HUMAN = "human"
VETERINARY = "veterinary"
class SubmissionFormat(str, Enum):
ECTD = "ectd"
NEES = "nees"
VNEES = "vnees"
@dataclass(frozen=True)
class FilingContext:
"""Everything the selector needs to resolve a format."""
jurisdiction: Jurisdiction
product_type: ProductType
is_legacy_sequence: bool = False # True only for a sequence continuing an existing NeeS lifecycle
@dataclass(frozen=True)
class FormatDecision:
format: SubmissionFormat
reason: str
class UnsupportedFilingError(RuntimeError):
"""Raised when no rule matches; never guess a format for a filing."""
class FormatSelector:
def __init__(self, rules: list[dict]) -> None:
self._rules = rules
@classmethod
def from_yaml(cls, rules_path: str) -> "FormatSelector":
with open(rules_path, "r", encoding="utf-8") as handle:
document = yaml.safe_load(handle)
return cls(document["rules"])
def select(self, context: FilingContext) -> FormatDecision:
for rule in self._rules:
if rule["jurisdiction"] != context.jurisdiction.value:
continue
if rule["product_type"] != context.product_type.value:
continue
legacy_format = rule.get("legacy_format")
if context.is_legacy_sequence and legacy_format:
decision = FormatDecision(
format=SubmissionFormat(legacy_format),
reason="continuing an existing legacy-format lifecycle",
)
else:
decision = FormatDecision(
format=SubmissionFormat(rule["format"]),
reason="current rule for jurisdiction and product type",
)
self._record_decision(context, decision)
return decision
raise UnsupportedFilingError(
f"no format rule for jurisdiction={context.jurisdiction.value} "
f"product_type={context.product_type.value}"
)
@staticmethod
def _record_decision(context: FilingContext, decision: FormatDecision) -> None:
event = {
"action": "submission_format_selected",
"jurisdiction": context.jurisdiction.value,
"product_type": context.product_type.value,
"is_legacy_sequence": context.is_legacy_sequence,
"format": decision.format.value,
"reason": decision.reason,
}
logger.info(json.dumps(event, sort_keys=True))
Two properties make this safe to run unattended. First, the engine never returns a default — an unmatched combination raises UnsupportedFilingError and stops the pipeline rather than shipping a guess. Second, every successful selection writes a single structured event, so a reviewer can later answer “why was this filing packaged as VNeeS” from a log line instead of a memory of a conversation.
Validation and edge-case handling
A handful of situations account for nearly every misrouted format in practice:
- A veterinary product filed alongside a human one under the same sponsor. The two must resolve independently — sharing a jurisdiction is not sharing a product type. Keep
ProductTypeon the filing, not on the sponsor or application record. - A national procedure sequence that started life as NeeS. A lifecycle continuation of an already-accepted NeeS sequence generally stays in that format for its remaining life rather than switching mid-stream; the
is_legacy_sequenceflag exists precisely so a renewal or variation on an old sequence does not get silently upgraded to eCTD in a way the authority’s own records do not expect. Confirm the specific authority’s transition guidance before setting this flag for a new application — it should almost never be true for a brand-new filing. - A jurisdiction not in the rule table. Raise, do not fall back to eCTD by convenience. A format assumption for an unmodeled market is exactly the kind of silent decision an inspector will ask you to justify.
- VNeeS mistaken for a deprecated format. Because VNeeS shares NeeS’s non-XML structure, a rule set copied from a human-only pipeline sometimes marks it “legacy” by association. It is not; treat it as the current, first-class veterinary format with its own row in the table, never as a fallback.
Testing and verification
Table-driven tests against the same YAML fixture the pipeline loads in production catch a bad rule edit before it reaches a real filing.
"""Tests for submission format selection."""
from __future__ import annotations
import pytest
@pytest.fixture
def selector(tmp_path) -> "FormatSelector":
rules_path = tmp_path / "format_rules.yaml"
rules_path.write_text(
"rules:\n"
" - jurisdiction: FDA\n"
" product_type: human\n"
" format: ectd\n"
" - jurisdiction: EMA_NATIONAL\n"
" product_type: human\n"
" format: ectd\n"
" legacy_format: nees\n"
" - jurisdiction: EMA_NATIONAL\n"
" product_type: veterinary\n"
" format: vnees\n",
encoding="utf-8",
)
return FormatSelector.from_yaml(str(rules_path))
def test_new_human_filing_selects_ectd(selector: "FormatSelector") -> None:
context = FilingContext(Jurisdiction.EMA_NATIONAL, ProductType.HUMAN, is_legacy_sequence=False)
decision = selector.select(context)
assert decision.format is SubmissionFormat.ECTD
def test_legacy_sequence_stays_nees(selector: "FormatSelector") -> None:
context = FilingContext(Jurisdiction.EMA_NATIONAL, ProductType.HUMAN, is_legacy_sequence=True)
decision = selector.select(context)
assert decision.format is SubmissionFormat.NEES
def test_veterinary_filing_selects_vnees(selector: "FormatSelector") -> None:
context = FilingContext(Jurisdiction.EMA_NATIONAL, ProductType.VETERINARY)
decision = selector.select(context)
assert decision.format is SubmissionFormat.VNEES
def test_unmodeled_combination_raises(selector: "FormatSelector") -> None:
context = FilingContext(Jurisdiction.FDA, ProductType.VETERINARY)
with pytest.raises(UnsupportedFilingError):
selector.select(context)
The four cases cover the paths that matter: the common current-format path, the legacy exception, the veterinary track, and the refusal to guess when nothing matches. Any change to the rule set should extend this fixture before it is trusted in production.
FAQ
Is NeeS still accepted for new human submissions?
Acceptance has been progressively withdrawn as eSubmission roadmaps push authorities toward eCTD for new filings; treat NeeS as valid only for continuing an already-accepted legacy sequence, and confirm the specific authority’s current transition guidance before assuming otherwise.
Why is VNeeS not being retired like NeeS?
VNeeS is the current, purpose-built format for veterinary medicinal product submissions, governed by its own regulation and technical annexes. It shares NeeS’s non-XML, folder-based structure by design, but that structural similarity does not make it a deprecated format — it is the format veterinary filings are expected to use today.
Can a single sponsor mix formats across applications?
Yes, and it is common: a sponsor with both human and veterinary products, or with older sequences still on a legacy lifecycle, will legitimately have some applications in eCTD, some in a retained legacy format, and some in VNeeS at the same time. That is exactly why format selection must be resolved per filing rather than set once per sponsor or system.