Skip to content

The METS files we write

This page is a specification of the METS files that the platform itself creates and maintains. It describes what the MetsManager writes when a Deposit has a managed METS file: one created from a Deposit template (RootLevel or BagIt) rather than supplied by a third party.

This is the normative profile. It is the shape every document the platform edits ends up in: the editability rules are defined against this page, and a third-party document that qualifies for editing is restructured to this profile on its first save.

For the METS files we can read — which is a much wider set, including Archivematica, EPrints and Goobi output — see What the METS parser can read.

The design principle throughout: the METS file is the canonical record of everything we know about the object — its files, their fixity and formats, tool output (format identification, virus scanning, Exif extraction), descriptive metadata, access conditions, and logical structure for presentation. The entire preservation state must be recoverable from the OCFL object alone, and the METS file inside it carries everything that isn’t the bitstreams themselves.

This page covers the document skeleton, IDs, the physical structure, and technical metadata. Descriptive metadata, logical structure, file links and a complete example are on the second page.

  • Creating a Deposit from a template writes a skeleton mets.xml at the deposit root (or at data/mets.xml for the BagIt template).
  • Adding a file or folder to the deposit (via the Preservation API or UI) adds corresponding entries.
  • Deleting a file or folder removes its entries.
  • Running the pipeline (Brunnhilde/Siegfried, ClamAV, Exif tools) patches technical metadata into the entries for each file.
  • Editing names, access conditions, rights, catalogue identifiers or logical structure updates the relevant sections.

A METS file is currently only treated as editable when its mets:agent name is exactly ours (see next section); third-party METS is never modified. That gate is agreed to change — editability decided by conformance rather than by the agent name, with defined classes of third-party METS becoming editable. See METS editability for the rules and their status.

Concurrent edits are guarded by the S3 ETag of the METS file: every read-modify-write cycle passes the ETag back, and a mismatch fails the operation rather than clobbering another writer’s change (see If-Match and ETags).

A brand-new managed METS file, before any files have been added:

<mets:mets xmlns:mets="http://www.loc.gov/METS/"
xmlns:mods="http://www.loc.gov/mods/v3"
xmlns:premis="http://www.loc.gov/premis/v3"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<mets:metsHdr CREATEDATE="2026-06-15T15:48:23.1549777+01:00">
<mets:agent ROLE="CREATOR" TYPE="OTHER" OTHERTYPE="SOFTWARE">
<mets:name>University of Leeds Digital Library Infrastructure Project</mets:name>
</mets:agent>
</mets:metsHdr>
<mets:dmdSec ID="DMD_PHYS_ROOT">
<mets:mdWrap MDTYPE="MODS">
<mets:xmlData>
<mods:mods>
<mods:titleInfo>
<mods:title>My New Deposit</mods:title>
</mods:titleInfo>
</mods:mods>
</mets:xmlData>
</mets:mdWrap>
</mets:dmdSec>
<mets:amdSec ID="ADM_objects">
<mets:techMD ID="TECH_objects">
<mets:mdWrap MDTYPE="PREMIS:OBJECT">
<mets:xmlData>
<premis:object xsi:type="premis:file">
<premis:objectCharacteristics />
<premis:originalName>objects</premis:originalName>
</premis:object>
</mets:xmlData>
</mets:mdWrap>
</mets:techMD>
</mets:amdSec>
<mets:amdSec ID="ADM_metadata"><!-- same shape, originalName = metadata --></mets:amdSec>
<mets:amdSec ID="ADM_metadata_x002F_ad-hoc"><!-- same shape, originalName = metadata/ad-hoc --></mets:amdSec>
<mets:fileSec>
<mets:fileGrp USE="OBJECTS" />
</mets:fileSec>
<mets:structMap TYPE="PHYSICAL">
<mets:div ID="PHYS_ROOT" LABEL="__ROOT" DMDID="DMD_PHYS_ROOT" TYPE="Directory">
<mets:div ID="PHYS_metadata" LABEL="metadata" DMDID="DMD_metadata" ADMID="ADM_metadata" TYPE="Directory">
<mets:div ID="PHYS_metadata_x002F_ad-hoc" LABEL="ad-hoc" DMDID="DMD_metadata_x002F_ad-hoc"
ADMID="ADM_metadata_x002F_ad-hoc" TYPE="Directory" />
</mets:div>
<mets:div ID="PHYS_objects" LABEL="objects" DMDID="DMD_objects" ADMID="ADM_objects" TYPE="Directory" />
</mets:div>
</mets:structMap>
</mets:mets>

Things to notice, each expanded on below:

  • The agent name identifies us as creator; this exact string is what makes a METS file editable by the platform.
  • The root title comes from the Deposit’s name (falling back to [Untitled]), held as a MODS title in DMD_PHYS_ROOT.
  • Three directories always exist: objects/ (the payload), metadata/ (pipeline tool output) and metadata/ad-hoc/ (arbitrary user-supplied metadata files).
  • Directory divs reference dmdSecs (DMD_metadata, DMD_objects, …) that do not exist yet — descriptive metadata sections are created lazily, the first time something is actually set on that resource.
  • There is a single mets:fileGrp with USE="OBJECTS"; all files go in it.
  • There is no mets:structLink and no logical structMap until they are needed.

All IDs are a fixed prefix per section, followed by the resource’s path within the deposit encoded with XmlConvert.EncodeLocalName so the result is a valid XML NCName: / becomes _x002F_, a space becomes _x0020_, and characters that are already legal are left alone.

Prefix Used for Path Minted ID
PHYS_ mets:div in the physical structMap objects/photo.tif PHYS_objects_x002F_photo.tif
FILE_ mets:file in the fileSec objects/photo.tif FILE_objects_x002F_photo.tif
ADM_ mets:amdSec objects/photo.tif ADM_objects_x002F_photo.tif
TECH_ mets:techMD inside the amdSec objects/photo.tif TECH_objects_x002F_photo.tif
DMD_ mets:dmdSec objects/photo.tif DMD_objects_x002F_photo.tif
digiprovMD_ClamAV_ mets:digiprovMD holding a virus-scan event digiprovMD_ClamAV_ + the amdSec’s ID; second and later scans of the same file insert an occurrence number between the two parts (digiprovMD_ClamAV_2_ADM_…)

A top-level folder needs no encoding, so the built-in IDs read exactly as before: PHYS_objects, ADM_metadata, TECH_objects. The ad-hoc metadata folder does contain a /, so it is PHYS_metadata_x002F_ad-hoc.

The root div is always PHYS_ROOT with label __ROOT, and its descriptive metadata is DMD_PHYS_ROOT. Logical structMap divs have caller-supplied IDs (conventionally LOG_0000, LOG_0001, …) and their dmdSecs are DMD_ + div ID (e.g. DMD_LOG_0001). Caller-supplied IDs are validated, not encoded — an ID that could not be an NCName is rejected with a 400, because the caller round-trips these and silently rewriting one would break its own references.

The convention still makes the METS broadly readable by path, but the platform does not rely on it: since #188 step 1 it navigates by a path cache built from premis:originalName (directories) and FLocat/@xlink:href (files), treating ID text as opaque. That is what makes it safe for one document to hold IDs in more than one form.

The physical structMap mirrors the deposit’s directory tree exactly.

  • Directories are <mets:div TYPE="Directory"> with a LABEL (a human-readable name, editable independently of the path), an ADMID and — once any descriptive metadata is set — a DMDID.
  • Files are <mets:div TYPE="Item"> containing exactly one <mets:fptr FILEID="FILE_{path}"/>. The LABEL is the file’s display name, which defaults to the filename but can be changed (e.g. LABEL="Tape 1 Side 1" for tape1side1.wav).
  • Children of a div are kept sorted alphabetically by label (case-insensitive); adding a file re-sorts its siblings.
  • A file or directory can only be added when its parent directory already exists in the METS; deletes are only permitted on files and empty directories.

A populated example:

<mets:structMap TYPE="PHYSICAL">
<mets:div ID="PHYS_ROOT" LABEL="__ROOT" DMDID="DMD_PHYS_ROOT" TYPE="Directory">
<mets:div ID="PHYS_metadata" LABEL="metadata" DMDID="DMD_metadata" ADMID="ADM_metadata" TYPE="Directory">
<mets:div ID="PHYS_metadata/ad-hoc" LABEL="ad-hoc" DMDID="DMD_metadata/ad-hoc"
ADMID="ADM_metadata/ad-hoc" TYPE="Directory" />
</mets:div>
<mets:div ID="PHYS_objects" LABEL="objects" DMDID="DMD_objects" ADMID="ADM_objects" TYPE="Directory">
<mets:div ID="PHYS_objects/amber-rudd.m4a" LABEL="Amber Rudd.m4a" TYPE="Item">
<mets:fptr FILEID="FILE_objects/amber-rudd.m4a" />
</mets:div>
<mets:div ID="PHYS_objects/angela-eagle.m4a" LABEL="Angela Eagle.m4a" TYPE="Item"
DMDID="DMD_objects/angela-eagle.m4a">
<mets:fptr FILEID="FILE_objects/angela-eagle.m4a" />
</mets:div>
</mets:div>
</mets:div>
</mets:structMap>

Note that DMDID appears on the file div only when that file actually has descriptive metadata (here, angela-eagle.m4a has its own access conditions — see the descriptive metadata page).

One file group, USE="OBJECTS". Each file entry carries its content type and points at its administrative metadata:

<mets:fileSec>
<mets:fileGrp USE="OBJECTS">
<mets:file ID="FILE_objects/photo.tif" MIMETYPE="image/tiff" ADMID="ADM_objects/photo.tif">
<mets:FLocat LOCTYPE="URL" xlink:type="simple" xlink:href="objects/photo.tif" />
</mets:file>
</mets:fileGrp>
</mets:fileSec>
  • xlink:href is the path of the file relative to the METS file’s own directory — for a RootLevel deposit that is the deposit root, for a BagIt deposit it is the data/ directory. Paths in the METS never include the data/ prefix.
  • MIMETYPE is the best content type we have — the value supplied at upload, refined by format identification when the pipeline runs. It is omitted when the type is not identified.
  • The METS file never describes itself: there is no mets:file entry for mets.xml. (The parser adds a synthetic entry for it when reading, so downstream code still sees it in the file list.)

Technical metadata: amdSec / techMD / PREMIS object

Section titled “Technical metadata: amdSec / techMD / PREMIS object”

Every file and every directory has one mets:amdSec containing one mets:techMD wrapping a PREMIS 3 premis:object of type premis:file, MDTYPE="PREMIS:OBJECT".

For a directory, the PREMIS object is minimal — it exists to anchor the directory’s path in premis:originalName:

<mets:amdSec ID="ADM_objects">
<mets:techMD ID="TECH_objects">
<mets:mdWrap MDTYPE="PREMIS:OBJECT">
<mets:xmlData>
<premis:object xsi:type="premis:file">
<premis:objectCharacteristics />
<premis:originalName>objects</premis:originalName>
</premis:object>
</mets:xmlData>
</mets:mdWrap>
</mets:techMD>
</mets:amdSec>

For a file, the PREMIS object accumulates everything we learn about it. A fully populated example (an image that has been through the pipeline):

<mets:amdSec ID="ADM_objects/photo.tif">
<mets:techMD ID="TECH_objects/photo.tif">
<mets:mdWrap MDTYPE="PREMIS:OBJECT">
<mets:xmlData>
<premis:object xsi:type="premis:file">
<premis:significantProperties>
<premis:significantPropertiesType>ImageWidth</premis:significantPropertiesType>
<premis:significantPropertiesValue>4032</premis:significantPropertiesValue>
</premis:significantProperties>
<premis:significantProperties>
<premis:significantPropertiesType>ImageHeight</premis:significantPropertiesType>
<premis:significantPropertiesValue>3024</premis:significantPropertiesValue>
</premis:significantProperties>
<premis:objectCharacteristics>
<premis:fixity>
<premis:messageDigestAlgorithm xsi:type="premis:messageDigestAlgorithm">SHA256</premis:messageDigestAlgorithm>
<premis:messageDigest>eb634d64ce8e6be5195174ceaef9ac9e19c37119f3b31618630aa633ccdbf68f</premis:messageDigest>
</premis:fixity>
<premis:size>9876543</premis:size>
<premis:format>
<premis:formatDesignation>
<premis:formatName xsi:type="premis:formatName">Tagged Image File Format</premis:formatName>
</premis:formatDesignation>
<premis:formatRegistry>
<premis:formatRegistryName xsi:type="premis:formatRegistryName">PRONOM</premis:formatRegistryName>
<premis:formatRegistryKey xsi:type="premis:formatRegistryKey">fmt/353</premis:formatRegistryKey>
</premis:formatRegistry>
</premis:format>
<premis:objectCharacteristicsExtension xsi:type="premis:objectCharacteristicsExtension">
<ExifMetadata>
<ImageHeight>3024</ImageHeight>
<ImageWidth>4032</ImageWidth>
</ExifMetadata>
</premis:objectCharacteristicsExtension>
</premis:objectCharacteristics>
<premis:originalName>objects/photo.tif</premis:originalName>
</premis:object>
</mets:xmlData>
</mets:mdWrap>
</mets:techMD>
</mets:amdSec>

Element by element:

Element Meaning Written when
premis:fixity SHA256 digest, algorithm always written as SHA256 On upload (client-supplied digest) and verified/updated by tools
premis:size Size in bytes On upload
premis:format PRONOM identification: human-readable formatName plus formatRegistry PRONOM / key (fmt/353) When Siegfried (via Brunnhilde) has identified the format
premis:originalName The file’s deposit-relative path Always
premis:storage / premis:contentLocation A URI locating the binary in preservation storage; storageMedium is set to our agent name so we can find our own storage assertion among others When known (e.g. METS exported from an Archival Group)
premis:significantProperties Typed name/value pairs — see next section When tool output or client metadata provides them
premis:objectCharacteristicsExtension Raw Exif dump — see next section When Exif extraction has run

Updates are patches, not rewrites: setting a new digest, size or format only touches those elements; anything else already in the PREMIS object (including significantProperties from other sources) is preserved.

Media extents are written as PREMIS significant properties with these type names:

  • ImageWidth, ImageHeight — pixel dimensions (images and video)
  • Duration — audio/video duration
  • Bitrate — audio bitrate (from the Exif AvgBitrate tag)

They can arrive from two directions — extracted from Exif tool output, or asserted by the client as extent metadata — and both are merged into the same elements. If a property is asserted twice with different values (e.g. Exif says one width, the client says another), the write fails with a conflict error rather than silently overwriting.

For video, the Exif-derived dimensions prefer the composite ImageSize tag, then SourceImageWidth/SourceImageHeight, then the first ImageWidth/ImageHeight — per-track tags in containers like MOV can be misleading (a timecode track reporting 853×20).

The complete Exif tool output is preserved as an XML blob inside premis:objectCharacteristicsExtension, under an <ExifMetadata> element with one child element per tag (tag names stripped to alphanumerics):

<premis:objectCharacteristicsExtension xsi:type="premis:objectCharacteristicsExtension">
<ExifMetadata>
<Duration>3:42</Duration>
<AvgBitrate>128 kbps</AvgBitrate>
</ExifMetadata>
</premis:objectCharacteristicsExtension>

Re-running Exif replaces this blob wholesale (it does not merge), but leaves significant properties from other sources alone.

Virus scan results are a PREMIS event, not part of the object. They live in a mets:digiprovMD in the same amdSec as the file’s techMD, with MDTYPE="PREMIS:EVENT" and the ID convention digiprovMD_ClamAV_{admId}:

<mets:digiprovMD ID="digiprovMD_ClamAV_ADM_objects/photo.tif">
<mets:mdWrap MDTYPE="PREMIS:EVENT">
<mets:xmlData>
<premis:event>
<premis:eventType>virus check</premis:eventType>
<premis:eventDateTime>2026-06-16T14:30:45Z</premis:eventDateTime>
<premis:eventDetailInformation>
<premis:eventDetail>ClamAV 1.4.3/27932/Fri Mar 6 07:24:27 2026</premis:eventDetail>
</premis:eventDetailInformation>
<premis:eventOutcomeInformation>
<premis:eventOutcome>Pass</premis:eventOutcome>
<premis:eventOutcomeDetail />
</premis:eventOutcomeInformation>
</premis:event>
</mets:xmlData>
</mets:mdWrap>
</mets:digiprovMD>
  • eventType is always virus check. This is what identifies the event as a scan when the document is read back — the reader matches the event type, not the ID.
  • eventDateTime is when the scan ran — an ISO 8601 UTC instant, taken from the ClamAV log the scan itself wrote. It is not the time the METS was written, and it does not change when the same scan output is read again. (Before August 2026 it was a locale-formatted date with no time, stamped at write time.)
  • eventDetail records the scanner and virus-definition versions.
  • eventOutcome is Pass or Fail; on Fail, eventOutcomeDetail/eventOutcomeDetailNote names the virus found.

Digital provenance is a history, not a current value. Scanning a file again appends a new digiprovMD alongside whatever is already in the amdSec — earlier scans, and provenance events from other sources or other tools entirely. Nothing already present is read, reordered, rewritten or removed, and events the platform does not recognise are left strictly alone. The current virus status of a file is therefore the most recent ClamAV event, not the only one.

Only an actual scan writes an event: an ordinary metadata update does not rewrite the last one.

One scan is one event, however often its output is read. The same file can pass through the METS writer again while the previous run’s ClamAV output is still sitting in the deposit, and a redelivered pipeline job does exactly that for every file in it. A scan already recorded — recognised by its eventDateTime, since two scans of one file cannot share an instant — is not recorded a second time. A genuine re-scan carries a later time and is appended as its own event, even when it found precisely what the last one found.

Because each event needs its own xs:ID, the second and later scans of one file carry an occurrence number, inserted between the prefix and the identifier:

<mets:digiprovMD ID="digiprovMD_ClamAV_ADM_objects/photo.tif"> <!-- first scan -->
<mets:digiprovMD ID="digiprovMD_ClamAV_2_ADM_objects/photo.tif"> <!-- rescan -->

That placement is deliberate and load-bearing. A trailing counter would be indistinguishable from part of a path: …ClamAV_ADM_objects/a_2 is both the second scan of objects/a and the plain conventional ID for a file genuinely named objects/a_2, so one file’s virus status could be reported as its neighbour’s. A conventional ID always has the identifier’s own prefix (ADM_, TECH_) straight after digiprovMD_ClamAV_, never a digit, so a numbered ID can never be mistaken for another file’s.

Continue to descriptive metadata, logical structure and links.