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.
When the platform writes METS
Section titled “When the platform writes METS”- Creating a Deposit from a template writes a skeleton
mets.xmlat the deposit root (or atdata/mets.xmlfor theBagIttemplate). - 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).
Document skeleton
Section titled “Document skeleton”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 inDMD_PHYS_ROOT. - Three directories always exist:
objects/(the payload),metadata/(pipeline tool output) andmetadata/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:fileGrpwithUSE="OBJECTS"; all files go in it. - There is no
mets:structLinkand no logical structMap until they are needed.
ID conventions
Section titled “ID conventions”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.
Physical structMap
Section titled “Physical structMap”The physical structMap mirrors the deposit’s directory tree exactly.
- Directories are
<mets:div TYPE="Directory">with aLABEL(a human-readable name, editable independently of the path), anADMIDand — once any descriptive metadata is set — aDMDID. - Files are
<mets:div TYPE="Item">containing exactly one<mets:fptr FILEID="FILE_{path}"/>. TheLABELis the file’s display name, which defaults to the filename but can be changed (e.g.LABEL="Tape 1 Side 1"fortape1side1.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).
fileSec
Section titled “fileSec”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:hrefis the path of the file relative to the METS file’s own directory — for aRootLeveldeposit that is the deposit root, for aBagItdeposit it is thedata/directory. Paths in the METS never include thedata/prefix.MIMETYPEis 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:fileentry formets.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.
Significant properties: extents
Section titled “Significant properties: extents”Media extents are written as PREMIS significant properties with these type names:
ImageWidth,ImageHeight— pixel dimensions (images and video)Duration— audio/video durationBitrate— audio bitrate (from the ExifAvgBitratetag)
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).
Exif extension blob
Section titled “Exif extension blob”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 scanning: digiprovMD / PREMIS event
Section titled “Virus scanning: digiprovMD / PREMIS event”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>eventTypeis alwaysvirus 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.eventDateTimeis 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.)eventDetailrecords the scanner and virus-definition versions.eventOutcomeisPassorFail; onFail,eventOutcomeDetail/eventOutcomeDetailNotenames the virus found.
Provenance events accumulate
Section titled “Provenance events accumulate”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.