Skip to content

Tidy a folder every night

What it does. Every night at 02:00 the script looks at an Inbox folder, takes the files that haven’t changed for 30 days, and moves them into Archive/<year>. It moves at most 200 files a run, so a big backlog clears over a few nights instead of timing out.

What you need.

  • The MaSH feature.
  • A schedule on Settings > MaSH > Schedule:
    1. Choose Add schedule, then pick the script.
    2. Set Frequency to Daily, Hour to 02:00, and your Timezone.
    3. Under Files to run on, choose Folder listing, then the share and the path (Inbox).
    4. Save.
  • Script settings: ARCHIVE (the archive folder, default Archive) and DAYS (default 30).

A schedule hands the script its files in mash.event.files, once per run. See Schedules.

The only real logic is “is this old enough?” and “where does it go?”. Both are easy to get wrong with dates, so test them with fixed dates first:

block isOldEnough modified days today
set cutoff to today.copy().subDays(days)
return modified.lt(cutoff)
end
block archiveFor root modified
return "{{root}}/" ~ modified.format("Y")
end
set today to mash.date.parse("2026-11-03")
each [["2026-09-01", "a.pdf"], ["2026-10-25", "b.pdf"], ["2025-12-30", "c.pdf"]] as pair
set modified to mash.date.parse(pair[0])
if isOldEnough(modified, 30, today)
printline "{{pair[1]}}: move to " ~ archiveFor("Archive", modified)
else
printline "{{pair[1]}}: keep"
end
end
a.pdf: move to Archive/2026
b.pdf: keep
c.pdf: move to Archive/2025

copy() matters. Dates in MaSH are mutable, so today.subDays(30) would change today itself. Work on a copy.

set errorblock to "onError"
block onError
printline "Error on line {{mash.error.line}}: {{mash.error.message}}"
end
block isOldEnough modified days today
set cutoff to today.copy().subDays(days)
return modified.lt(cutoff)
end
set archiveRoot to mash.config.get("ARCHIVE") ?? "Archive"
set days to mash.config.get("DAYS") ?? 30
set maxPerRun to 200
set files to mash.event.files
if not files or count(files) is 0
printline "Nothing in the folder."
return
end
set today to mash.date.now()
set moved to 0
set kept to 0
each files as doc
if moved is more than or equal to maxPerRun
break
end
if doc.is_dir
next
end
if not isOldEnough(doc.modified, days, today)
set kept to kept + 1
next
end
set year to doc.modified.format("Y")
set yearFolder to mash.files.get(doc.share, archiveRoot).make(year, true)
# Never overwrite a file already in the archive.
if mash.files.exists(doc.share, yearFolder.path ~ "/" ~ doc.name)
printline "{{doc.name}} is already in {{yearFolder.path}}, leaving it."
next
end
doc.move(yearFolder)
set moved to moved + 1
end
printline "Moved {{moved}}, kept {{kept}}."
if mash.event.files_truncated
printline "More files matched than were handed over. The next run will carry on."
end
if moved is more than or equal to maxPerRun
printline "Hit the limit of {{maxPerRun}} for this run. The rest wait for the next one."
end
  1. The batch cap (maxPerRun) keeps the run inside its time limit. A queued run has 60 seconds. Moving a file takes a moment, and a large backlog would hit the limit and stop in the middle. See Limits.
  2. Moved files leave the folder. Tomorrow’s listing no longer contains them, so the next run carries on from where this one stopped. That’s the whole trick for big jobs: take a bounded batch, and let the schedule start the next.
  3. files_truncated says when the schedule’s own list was cut at 1,000 files. Both caps lead to the same behaviour, a message and another go tomorrow.
  4. doc.move(yearFolder) takes the folder object, not a path in text. A path would be read on the file’s own share.
  5. next and break skip a file and leave the loop. MaSH calls the first one next, not continue. See Statements.
  6. Idempotent. Running the script twice in one night is harmless: the second run finds less to do.
  • Delete instead of archive. Use doc.delete(), but only after you’ve watched the move version behave for a week.
  • Tidy by a field. Use Files matching field criteria in the schedule, for example Status = Closed, and drop the age test.
  • Report what moved. Collect the names in a list and write them to a CSV. See Log each document’s fields to a CSV.
  • Warn before deleting. Run a second, weekly schedule that emails the list of files that will go next week. See Send an email from a template.
  • Stop a second copy starting. Add a script setting MASH_SINGLE_INSTANCE set to true. A new scheduled start is then skipped while the last is still running.

Library pages used: Files, Date, WrappedCarbon.