Skip to content

Log each document's fields to a CSV

What it does. Each time a document’s fields are filled in, the script adds one row to a CSV file: when it was logged, which share and path, the file name, and the fields you choose. People open the CSV in Excel. Another system can pick it up. A document is logged once.

What you need.

  • The MaSH feature.
  • A log share to hold the CSV. Keep it away from the share the documents arrive on (see “Why a different share” below).
  • Triggers on the documents’ share: Captur Extracted if Captur fills the fields, Fields Changed if people do. See Events.
  • Script settings: CSV_SHARE (required), CSV_NAME (default log.csv) and FIELDS (the field names, in column order, separated by commas).

A CSV writer starts empty or from existing text, takes one row at a time, and turns into text. You can see the shape without a share:

set header to ["Logged at", "Share", "Path", "File name", "Customer", "Order No"]
set row to ["2026-11-03 09:15:00 +00:00", "Claims", "Inbox/n1.pdf", "n1.pdf", "Acme, Ltd", "SO-1"]
set writer to mash.csv.writer()
writer.insertOne(header)
writer.insertOne(row)
printline writer.toString()
"Logged at",Share,Path,"File name",Customer,"Order No"
"2026-11-03 09:15:00 +00:00",Claims,Inbox/n1.pdf,n1.pdf,"Acme, Ltd",SO-1

The writer adds quotes where a value holds a comma or a space, so a customer called Acme, Ltd stays in one cell.

set errorblock to "onError"
block onError
printline "Error on line {{mash.error.line}}: {{mash.error.message}}"
end
set csvShare to mash.config.get("CSV_SHARE")
set csvName to mash.config.get("CSV_NAME") ?? "log.csv"
set fieldList to mash.str(mash.config.get("FIELDS") ?? "").trim().toString()
if not csvShare
fail "CSV_SHARE is not set on this automation."
end
set doc to mash.event.file
if not doc
return
end
if doc.is_dir
return
end
if doc.name is csvName
return
end
# The columns.
set wanted to []
each fieldList.split(",") as name
set name to mash.str(name).trim().toString()
if name is not ""
wanted[] = name
end
end
# The values. Indexing a field the share doesn't have is an error, so check.
set values to []
set captured to false
each wanted as name
set cell to ""
if doc.fields.has(name)
set parts to []
each mash.collect(doc.fields[name] ?? []) as v
set v to mash.str(v).trim().toString()
if v is not ""
parts[] = v
end
end
set cell to mash.collect(parts).join("; ")
end
if cell is not ""
set captured to true
end
values[] = "{{cell}}"
end
# Nothing filled in yet: log nothing, and wait for the next trigger.
if not captured
return
end
set header to ["Logged at", "Share", "Path", "File name"]
each wanted as name
header[] = name
end
set docShare to "{{doc.share.name}}"
set docPath to "{{doc.path}}"
set row to [mash.date.now().format("Y-m-d H:i:s P"), docShare, docPath, "{{doc.name}}"]
each values as v
row[] = v
end
if mash.files.exists(csvShare, csvName)
set csvFile to mash.files.get(csvShare, csvName)
set existing to "{{csvFile.contents}}"
if mash.str(existing).trim().length() is more than 0
set reader to mash.csv.read(csvFile)
reader.setHeaderOffset(0)
set haveHeader to mash.collect(reader.getHeader()).join("|")
set wantHeader to mash.collect(header).join("|")
if haveHeader is not wantHeader
fail "{{csvName}} has different columns. Rename it and the next document starts a new one."
end
# Already logged? A repeat trigger finds its own row.
each reader.getRecords() as record
set rowShare to record["Share"]
set rowPath to record["Path"]
if "{{rowShare}}" is docShare and "{{rowPath}}" is docPath
return
end
end
end
# A hand-edited CSV may not end in a newline.
if existing is not "" and mash.str(existing).endsWith("\n") is false
set existing to "{{existing}}\n"
end
set writer to mash.csv.writer(existing)
writer.insertOne(row)
csvFile.write(writer)
else
set writer to mash.csv.writer()
writer.insertOne(header)
writer.insertOne(row)
mash.files.get(csvShare, "").write(csvName, writer)
end
printline "Logged {{doc.name}} to {{csvName}}"
  1. Columns come from a setting, so one script serves several shares with different fields. Each share’s automation has its own FIELDS value.
  2. A multi-value field (several parties on one claim) is joined with ; into one cell.
  3. captured stays false until at least one field has a value. Without it, the first trigger would add a row of empty cells.
  4. The header check protects the file. If you rename a field or change FIELDS, the columns no longer match, and the script stops with a clear message instead of adding a crooked row.
  5. The duplicate check reads each existing row and returns if the share and path are already there. The first capture is what’s logged. A later correction doesn’t change the row.
  6. The write reads the whole file, adds one row and writes it all back. MaSH has no append and no lock.

The CSV is a new file, and a new file is crawled and can fire events. The doc.name == csvName guard stops the script logging its own CSV. Keeping the CSV off the watched share keeps it out of the way entirely.

Two documents that arrive in the same instant can both read the same CSV, and the later write wins. One row is lost, with no error. At a few documents a minute this won’t happen. For a share that takes hundreds of documents an hour, write one small CSV per document into a folder, and join them afterwards.

  • Update instead of skip. To keep the latest values, rebuild the CSV from the old records, replacing the matching row. It costs more per run.
  • An Excel file. Use mash.xlsx or mash.sheet. See Build a spreadsheet report of a folder.
  • Only some documents. Return unless the file name or a field contains a word. Filtering with the trigger’s Path prefix and Extension is cheaper.

Library pages used: CSV, Files, Fields.