A spreadsheet carries two layers of identity. There is the grid of cells, and there is the document metadata that rides alongside it: title, author, company, keywords, the timestamps. Excel never shows that second layer in the grid, yet it is the layer Windows Search indexes, the one SharePoint reads to title a document, and the one a records-management system files by. When a generated workbook inherits its Author and Title from the template it was built from, every downstream system records the template designer as the author of four thousand customer statements. The metadata is correct nowhere and consulted everywhere.
HotXLS surfaces this layer as ordinary workbook-level properties on both of its engines: the BIFF facade for .xls and the OOXML facade for .xlsx. You read a field after opening a file and you write a field before saving one. The library decides which physical container the value lands in. What is worth understanding before you write a generator is which fields each format actually supports, where those fields physically live, and the one gating rule that governs whether an .xlsx records any metadata at all.
Two formats, two storage models
The reason a spreadsheet library needs two metadata implementations, and the reason half-finished tools stamp one format correctly and forget the other, is that .xls and .xlsx keep their properties in unrelated places. A BIFF workbook writes them into OLE compound-file streams, chiefly the SummaryInformation property set that predates Excel itself, alongside the in-stream WRITEACCESS record that names whoever last saved the file. An OOXML workbook keeps them as XML parts inside the zip package, split by purpose: docProps/core.xml holds the Dublin Core fields (title, creator, subject, keywords, dates) and docProps/app.xml holds the application-level fields such as company and generating application, per ECMA-376 Part 1.
HotXLS flattens both of those storage models into direct properties of the workbook object. You never open a property-set stream or edit an XML part by hand. You assign strings and dates to the workbook, and the correct container materializes for whichever format you save.
Stamping generated workbooks from the business record
On the XLSX side, TXLSXWorkbook exposes Title, Subject, Author, Keywords, Description, Category, LastModifiedBy, Company, Application, and AppVersion as strings, plus Created and Modified as TDateTime values where zero means unset. The rule that closes the inheritance hole is one sentence: assign every field on every run, taking the values from the business record instead of trusting whatever the template happened to carry.
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open('statement-template.xlsx') <> 1 then
raise Exception.Create('Template not available');
// Overwrite every field: anything left untouched is
// inherited from whoever designed the template.
Book.Title := 'Account Statement 2026-06 / ACME Corp';
Book.Subject := 'Monthly account statement';
Book.Author := 'Billing Service 4.2';
Book.LastModifiedBy := 'Billing Service 4.2';
Book.Company := 'Northwind Financial';
Book.Category := 'Customer Delivery';
Book.Keywords := 'statement;billing;2026-06;acct-10024';
Book.Description := 'Generated document - manual edits are not retained';
Book.Created := Now;
Book.Modified := Now;
Book.SaveAs('statement-10024.xlsx');
finally
Book.Free;
end;
end;
The Keywords field repays more thought than it usually gets. Search infrastructure indexes it verbatim, Windows Search, SharePoint, and most DMS products alike, so a semicolon-separated convention carrying the account number and period turns every delivered workbook into a findable record with no database round trip. The same reach is the catch. Properties travel with every copy of the file, well past the access controls of the system that wrote them, so personal data does not belong in there.
The timestamp pair carries semantics worth fixing in policy rather than leaving to habit. Created should mark the moment your pipeline generated the document and then stay frozen. Modified is the field Excel updates whenever a recipient saves the file, so a divergence between the two after delivery is positive evidence that someone edited the workbook downstream, which settles more than one dispute about whose numbers a forwarded spreadsheet really holds. One trap hides in the unset state: it is the literal value zero, not an exception and not a null, so audit code has to test for zero explicitly. Format an unset TDateTime without that guard and your logs fill with a confidently wrong December 1899 date.
DocPropsTouched: the workbook that ships without docProps
A read-only flag, DocPropsTouched, gates the XLSX property writer. A workbook in which no property was ever assigned produces no docProps parts whatsoever; HotXLS declines to write an empty metadata skeleton. The behavior is tidy, and it has two consequences worth designing around.
Intake code on the consuming side must not assume core.xml exists in every package. A tool that hard-requires it will reject perfectly valid minimal files. And if your compliance posture demands that every outbound document carry at least a generator identity, that demand becomes code rather than a property of the format: assign Application and Author unconditionally in the save path, since an untouched workbook is entirely legal under the specification while quietly violating your policy.
The legacy XLS surface and the Comments trap
The BIFF facade carries the older, smaller field set: Title, Subject, Author, Keywords, Comments, Company, and Manager, plus LastSavedBy, an alias of UserName, which writes the WRITEACCESS record Excel displays when another user has the file locked.
var
Legacy: IXLSWorkbook; // reference-counted interface: no manual Free
begin
Legacy := TXLSWorkbook.Create;
if Legacy.Open('archive-1999.xls') <= 0 then
raise Exception.Create('Cannot open archive file');
Legacy.Title := 'FY1999 ledger (migrated copy)';
Legacy.Author := 'Archive Migration Batch';
Legacy.Company := 'Northwind Financial';
Legacy.Comments := 'Migrated 2026-06-11; source retained in cold storage';
Legacy.LastSavedBy := 'migration-svc'; // BIFF WRITEACCESS record
Legacy.SaveAs('archive-1999-stamped.xls');
end;
One naming collision causes recurring confusion. The document-level Comments property here is the free-text remark shown in the file's property dialog. It has nothing to do with cell comments, which are drawing-layer objects attached to ranges through a completely separate API. A code review that accepts "we already write Comments" without checking which one is meant has accepted a claim about the wrong feature, and that happens more often than the shared name would suggest. The two share four letters and not one byte of storage.
Reading metadata at intake, and the probe gap
Reading is symmetric. After Open, the same properties come back populated from the file, which turns a metadata audit of incoming workbooks into a short loop.
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open(FileName) = 1 then
begin
Writeln(Format('%s | title="%s" author="%s" created=%s',
[ExtractFileName(FileName), Book.Title, Book.Author,
FormatDateTime('yyyy-mm-dd', Book.Created)]));
if Book.Created = 0 then
Writeln(' no creation date recorded');
end;
finally
Book.Free;
end;
end;
Plan around one limitation while you do. There is no properties-only probe. GetSheetNames can list sheets without loading a workbook, but reading Title or Author means a full Open, so metadata triage across a large archive pays the full parse cost on every file. On the BIFF side you can trim that cost for read-only audits by setting _DisableGraphics to true before opening, which skips the drawing layer outright. It fits a loop that only reads properties and cell statistics, and it is exactly wrong the moment the same instance might save, because the skipped drawing content would be dropped. When sheet structure alone can pre-filter the set, single-sheet exports being the obvious thing to skip, the cheap techniques in our article on sheet listing and lightweight inspection cut how many files reach the expensive pass. And on bulk stamping jobs, where thousands of outputs are written rather than inspected, the write-side throughput patterns in our article on streaming writes for batch jobs carry over without change, since property assignment adds nothing measurable to save time.
Crossing formats and containing the leak
Properties round-trip cleanly inside a single facade: open an .xlsx, edit it, save it, and the set comes back intact. Crossing formats is where the assumption of parity breaks, because the BIFF and OOXML field sets do not line up one to one. BIFF has Manager and no timestamps; OOXML has Category, Description, and the Created/Modified pair. A converter that copies blindly loses whatever the destination format cannot hold, so map the fields explicitly and put the mapping in your conversion checklist next to everything else that does not survive the trip.
The leak that template inheritance opens runs the other way: information you never meant to ship out. Author names, internal project labels parked in keywords, a draft title nobody cleared. The overwrite-everything discipline from the generator above is the whole defense, and it is worth verifying the way an outsider would, by opening the Properties dialog any customer can reach or by unzipping the .xlsx and reading docProps/core.xml straight from the package. What you see there is exactly what every indexer downstream sees.
That downstream visibility is also the reason a few fields earn more care than the rest. Title, Author, Keywords (which surface as Tags), and Comments or Description carry most of the indexing weight in SharePoint and Windows Search. A Title that is genuinely distinct per document, carrying the period and the account, does more for findability than any folder-naming scheme stacked on top of it, and it costs one assignment per save.
Document properties are the cheapest professional polish a generated workbook can carry, and the most commonly shipped defect when nobody owns them. Both property surfaces described here belong to the HotXLS Component, which writes them natively for XLS and XLSX without Excel automation.