Aligning Horizon Europe Open Data Mandates

Horizon Europe’s open-science obligations read as principles — deposit research data in a trusted repository, make it FAIR, license it openly by default, keep it “as open as possible, as closed as necessary” — and principles do not fail a build. What fails an audit is a specific missing field: a deposit with no funding reference to the grant, a restricted dataset with no recorded justification, or an “open” record carrying a non-open license. This page turns each Horizon Europe requirement into a concrete metadata control and a mechanical validation, so a deposit either satisfies the mandate in checkable fields or is stopped before it publishes. The output is a compliance checker that reads a deposit record and reports exactly which requirements it meets and which it violates.

It sits beneath the broader Funder Mandate Alignment guide and assumes you already run a deposit pipeline and are comfortable with Python 3.10+ and the Pydantic V2 API. Two neighbouring guides carry the pieces this checker depends on: the plan behind the deposit comes from Building a Data Management Plan Template for Researchers, and the same requirement-to-property discipline for a different funder is worked in Mapping NIH DMS Policy Fields to DataCite Metadata. Here the funder is the European Commission and the emphasis is validation: what to check, and how to fail a non-compliant record loudly.

Horizon Europe deposit compliance flow A deposit record enters a compliance checker that runs one control per Horizon Europe requirement against the metadata. A decision node asks whether all controls pass. Passing records proceed to publication; failing records emit a violation report routed to remediation. Deposit record metadata Compliance checker control per rule All pass? yes Publish compliant no Violation report remediate

Requirements, Controls, and Validations

Horizon Europe’s data obligations flow from its Grant Agreement and the associated open-science annex. Each is a policy sentence, but each also implies a field that must be present and a rule that field must satisfy. The table below is the authoritative reference for this operation: it names each requirement, the metadata control that encodes it, and the validation a checker runs. The “as open as possible, as closed as necessary” principle is the one that resists a naive check — it does not demand openness unconditionally, it demands that any closedness be justified — so its control is conditional: a restricted access level is permitted only when a documented reason accompanies it.

Horizon Europe requirement Metadata control Validation
Deposit in a trusted repository publisher names a recognised repository publisher is present and in the trusted-repository allowlist
Persistent identifier Dataset carries a DOI An identifier of type DOI is present and well-formed
Open licence by default (CC-BY / CC0) rightsList SPDX id in the open default set rightsIdentifier is CC-BY-4.0 or CC0-1.0
Data Management Plan exists relatedIdentifiers links the DMP A related identifier with relationType IsDocumentedBy is present
FAIR / rich metadata Core descriptive fields populated title, creators, and at least one subject are non-empty
Metadata open even if data closed Metadata itself is not embargoed metadata_access equals open regardless of data access
EU funding acknowledged fundingReferences records the grant A funding reference names the funder and the grant/award number
As open as possible, as closed as necessary Restriction carries a justification If data_access is not open, a non-empty access_justification is present

Two rows separate a genuine Horizon Europe check from a generic open-data check. Metadata must stay open even when the data are restricted, so the metadata-access control is evaluated independently of the data-access level — a closed dataset with embargoed metadata violates the mandate even though the data restriction itself may be perfectly justified. And the funding acknowledgment is not a free-text credit line; it is a fundingReferences entry that must carry both the funder and the specific grant number, because that pairing is what links the dataset to the Commission’s reporting.

Production Implementation

The checker models a deposit with Pydantic V2 so structurally broken records are rejected before any policy check runs, then applies one predicate per requirement. Each control returns a structured result rather than raising, so the report lists every violation at once — a deposit missing both a licence and a funding reference should surface both, not stop at the first. The trusted-repository allowlist and the open-default licence set are the two policy knobs an institution tunes.

python
from __future__ import annotations

from dataclasses import dataclass
from typing import Callable, Final

from pydantic import BaseModel, Field

TRUSTED_REPOSITORIES: Final[frozenset[str]] = frozenset(
    {"Zenodo", "Dryad", "Dataverse", "B2SHARE", "PANGAEA"}
)
OPEN_DEFAULT_LICENSES: Final[frozenset[str]] = frozenset({"CC-BY-4.0", "CC0-1.0"})


class FundingReference(BaseModel):
    funder_name: str
    award_number: str


class DepositRecord(BaseModel):
    """The subset of a deposit's metadata a Horizon Europe check evaluates."""

    title: str = ""
    creators: list[str] = Field(default_factory=list)
    subjects: list[str] = Field(default_factory=list)
    publisher: str = ""
    doi: str = ""
    license_id: str = ""
    dmp_related_id: str = ""       # relatedIdentifier, relationType=IsDocumentedBy
    metadata_access: str = "open"  # "open" | "restricted"
    data_access: str = "open"      # "open" | "embargoed" | "restricted"
    access_justification: str = ""
    funding: list[FundingReference] = Field(default_factory=list)


@dataclass(frozen=True)
class CheckResult:
    requirement: str
    passed: bool
    detail: str


def _doi_well_formed(doi: str) -> bool:
    return doi.startswith("10.") and "/" in doi


# One predicate per Horizon Europe requirement; each returns (passed, detail).
_CONTROLS: Final[dict[str, Callable[[DepositRecord], tuple[bool, str]]]] = {
    "Trusted repository": lambda r: (
        r.publisher in TRUSTED_REPOSITORIES,
        f"publisher={r.publisher!r}",
    ),
    "Persistent identifier": lambda r: (
        _doi_well_formed(r.doi),
        f"doi={r.doi!r}",
    ),
    "Open licence by default": lambda r: (
        r.license_id in OPEN_DEFAULT_LICENSES,
        f"license={r.license_id!r}",
    ),
    "Data Management Plan linked": lambda r: (
        bool(r.dmp_related_id),
        "DMP related identifier present" if r.dmp_related_id else "no DMP link",
    ),
    "Rich FAIR metadata": lambda r: (
        bool(r.title and r.creators and r.subjects),
        "title/creators/subjects populated",
    ),
    "Metadata open even if data closed": lambda r: (
        r.metadata_access == "open",
        f"metadata_access={r.metadata_access!r}",
    ),
    "EU funding acknowledged": lambda r: (
        any(f.funder_name and f.award_number for f in r.funding),
        f"{len(r.funding)} funding reference(s)",
    ),
    "As open as possible, closed as necessary": lambda r: (
        r.data_access == "open" or bool(r.access_justification),
        f"data_access={r.data_access!r}, justified={bool(r.access_justification)}",
    ),
}


def check_compliance(record: DepositRecord) -> list[CheckResult]:
    """Run every Horizon Europe control and return a result per requirement."""
    results: list[CheckResult] = []
    for requirement, predicate in _CONTROLS.items():
        passed, detail = predicate(record)
        results.append(CheckResult(requirement, passed, detail))
    return results


def is_compliant(record: DepositRecord) -> bool:
    return all(r.passed for r in check_compliance(record))

The design decision that matters is separating check_compliance, which reports, from is_compliant, which gates. A pipeline calls the gate to decide whether to publish, but logs the full report so a rejected deposit tells the researcher every field to fix in one pass. The conditional “closed as necessary” control shows why a boolean-per-requirement model beats a single pass/fail flag: a restricted dataset is compliant precisely when it carries a justification, and the report makes that requirement legible rather than mysterious.

Verification

Drive the checker with a fully compliant record and a deliberately restricted one, asserting that an open record passes and that a restriction without justification is the only failure it produces.

python
def _base_record(**overrides: object) -> DepositRecord:
    data = dict(
        title="Alpine Soil Carbon Flux 2024",
        creators=["Ferreira, L."],
        subjects=["soil science"],
        publisher="Zenodo",
        doi="10.5281/zenodo.987654",
        license_id="CC-BY-4.0",
        dmp_related_id="10.5281/zenodo.111111",
        funding=[FundingReference(funder_name="European Commission", award_number="101099999")],
    )
    data.update(overrides)
    return DepositRecord(**data)  # type: ignore[arg-type]


def test_open_record_is_compliant() -> None:
    assert is_compliant(_base_record()) is True


def test_unjustified_restriction_fails_only_that_control() -> None:
    record = _base_record(data_access="restricted", access_justification="")
    failed = [r.requirement for r in check_compliance(record) if not r.passed]
    assert failed == ["As open as possible, closed as necessary"]


if __name__ == "__main__":
    for r in check_compliance(_base_record(license_id="CC-BY-NC-4.0")):
        mark = "PASS" if r.passed else "FAIL"
        print(f"[{mark}] {r.requirement}: {r.detail}")

Run the tests with pytest -q test_horizon_compliance.py; a passing run prints 2 passed. The __main__ block reports each control for a record carrying a non-commercial licence, and the open-licence control prints FAIL because CC-BY-NC-4.0 is not in the open-default set — the exact structured signal a deposit gate should treat as a hard block, since a non-commercial clause is not “open” under the mandate.

Gotchas

  • Metadata access is checked separately from data access. A justified data restriction does not license embargoing the metadata; the record is still non-compliant if its metadata is not open. Fix: evaluate the metadata-access control independently, so closed data with open metadata passes and closed data with closed metadata fails.
  • A non-commercial or no-derivatives licence is not “open by default”. CC-BY-NC-4.0 and CC-BY-ND-4.0 look Creative Commons but fall outside Horizon Europe’s CC-BY/CC0 default. Fix: validate the SPDX identifier against an explicit open-default set rather than a substring match on “CC”.
  • “Closed as necessary” needs a recorded reason, not just a restricted flag. Setting a dataset to restricted without a justification silently violates the balance the mandate requires. Fix: make the justification field mandatory whenever the access level is anything other than open, exactly as the conditional control does.

Frequently Asked Questions

Does Horizon Europe require every dataset to be fully open?

No. The standard is “as open as possible, as closed as necessary”, which permits restricting access when there is a legitimate reason such as personal data, security, or IP protection. What the mandate does require is that any restriction be justified and recorded, and that the metadata describing the dataset stay open even when the data themselves are closed. The compliance control therefore accepts a restricted dataset only when it carries a non-empty access justification, and it checks metadata openness independently of the data access level.

Which licences count as open by default under Horizon Europe?

The defaults are Creative Commons Attribution 4.0 (CC-BY-4.0) and the Creative Commons Zero public-domain dedication (CC0-1.0). Licences with additional restrictions — non-commercial (CC-BY-NC-4.0) or no-derivatives (CC-BY-ND-4.0) clauses — do not satisfy the open-by-default expectation even though they are Creative Commons licences. The checker validates the SPDX identifier against an explicit open-default set rather than matching on the “CC” prefix, so a restrictive Creative Commons variant is correctly reported as non-compliant.

How is the EU grant acknowledgment represented in the metadata?

It is a structured fundingReferences entry that names the funder and the specific grant or award number, not a free-text acknowledgment line. Both parts are required: a funder name without an award number, or an award number without a funder, fails the control. Recording the pairing in structured metadata is what links the dataset to the Commission’s reporting and lets an automated check confirm the acknowledgment is present rather than inferring it from prose.

See the Open Science Infrastructure Planning overview for how funder alignment fits the wider governance and deposit workflow.