Apple Photos on a Mac lets us export using photo titles as file names. That's great, but unfortunately there's no way to set the titles other than one-by-one manual entry in the "Info" panel or to set the titles for all selected items to the same value. So, I wrote the script below that allows me to set the titles and/or captions of all selected images or videos based on stored information specific to each one. This information is taken directly from the Photos database. Getting values directly from the database also allows circumventing the problem of using the "date" value provided by the Photos application to AppleScript. Specifically, if a photo was taken in a time zone that is not the one where a script is being run, then the date/time value is adjusted to the current time zone, resulting in a wrong value. What is retrieved from "date" and what is shown in the Photos "Info" panel will not match.
This script works by asking for a title "format string" that can contain substitution tags to represent specific values for each photo. Here is a list of them.
%D - Date as shown in the Photos information panel
%T - Time, including seconds
%t - Time, excluding seconds
%Z - Time zone
%XD - The original EXIF date (ie, unadjusted)
%XT - The original EXIF time, including seconds
%Xt - The original EXIF time, excluding seconds
%L - Latitude
%l - Longitude
%F - Original file name
%f - Photos internal file name
%fp - Photos internal file path
%M - Photos "Moment" name
%CT - Current title text
%CC - Current caption text
Here is an example of a format string that I use a lot: "%D %t | %M". This sets each selected photo's title to its creation date and time followed by "|" and then by the Photos "Moment" name. Here is another example: "Hiking trip - GPS: %L, %l"
The format string is remembered from one use to the next.
The Photos app must be open already for the script to run. The specific Photos library you are using will be automatically detected.
You can either run this script in Script Editor or, better, copy it into a "Run AppleScript" box in Automator to be used as a Quick Action for the Photos app. This allows you to call it from the Photos "Services" menu. You can also assign a shortcut key for this in your System Settings.
The first time you run it, you may be asked to allow Automator (or Script Editor) full access to your Photos. Running scripts in Automator can be tricky due to macOS's strict security sandboxing. If you have any issues getting it to run, let me know. There are workarounds.
--------------------------------
-- Photos • Set Title
--------------------------------
-- P.G. 2026-07-05
--
-- Set the titles for selected media to a value customized using substitution tags in a user-provided format string.
--
-- This script retrieves data from the currently open Photos library's database and from the "moments" information provided by the application.
--
-- Substitution tags:
-- %D - Date as shown in the Photos information panel
-- %T - Time, including seconds
-- %t - Time, excluding seconds
-- %Z - Time zone
-- %XD - The EXIF date of the original file
-- %XT - The EXIF time, including seconds
-- %Xt - The EXIF time, excluding seconds
-- %L - Latitude
-- %l - Longitude
-- %F - Original file name
-- %f - Photos internal file name
-- %fp - Photos internal file path
-- %M - Photos "Moment" name
-- %CT - Current title text
-- %CC - Current caption text
----------------------------------------
-- Get path to open library
--
set libPath to do shell script "x=$(lsof -p $(pgrep -if Photos.app/Contents/MacOS/Photos) | grep -m1 '.photoslibrary/resources'); x=/${x#*/}; x=${x%/resources*}; echo $x"
if libPath is "/" then
beep
display alert "No Photos library is open." as critical
return
end if
----------------------------------------
-- Get path to database file
--
set dbPath to libPath & "/database/Photos.sqlite"
tell application "System Events"
if not (exists file dbPath) then
beep
display alert "The Photos library database file cannot be located." & return & return & dbPath as critical
return
end if
end tell
----------------------------------------
-- Main process
--
tell application "Photos"
activate
----------------------------------------
-- Ensure selected items
--
if the (count of selection) is less than 1 then
beep
display dialog "No item is selected." & return & return & "Please select one or more media items for which you wish to set the title before invoking this service." buttons {"Cancel"} default button "Cancel" cancel button "Cancel" with title "Set Title" with icon caution
return
end if
----------------------------------------
-- Ask for format string
--
-- Tags: %D, %T, %t, %Z, %XD, %XT, %Xt, %L, %l, %F, %f, %fp, %M
--
set fmtPrevious to do shell script "f=~/.Photos_SetTitle; [ -e $f ] && cat $f || echo '%D %t | %M'"
set fmtString to ""
set setTarget to "Title"
repeat while fmtString is ""
display dialog "Specify your custom text or accept the previous value shown. You can use the tags shown below to include the indicated values taken from the Photos database." & return & return & "%D • Date as shown in the Photos information panel" & return & "%T • Time (including seconds) *" & return & "%t • Time (excluding seconds) *" & return & "%Z • Time zone" & return & "%XD • The EXIF date of the original file" & return & "%XT • EXIF time (including seconds) *" & return & "%Xt • EXIF time (excluding seconds) *" & return & "%L • GPS latitude" & return & "%l • GPS longitude" & return & "%F • Original file name" & return & "%f • Photos internal file name" & return & "%fp • Photos internal file path *" & return & "%M • Photos \"Moment\" name" & return & "%CT • Current title text" & return & "%CC • Current caption text" & return & return & "* When exporting items using the title for file names, Photos converts colons and forward slashes to hyphens. Colons are used here for substituting time values, whereas a √ replaces the forward slash in internal file path names in order to avoid ambiguity with other hyphens." & return & return & "Use \"Select\" to toggle settting the title, the caption, or both." default answer fmtPrevious buttons {"Cancel", "Select", "Set"} default button 3 cancel button "Cancel" with title "Set " & setTarget
set {fmtButton, fmtString} to {button returned of result, text returned of result}
if fmtButton is "Select" then
if setTarget is "Title" then
set setTarget to "Caption"
else if setTarget is "Caption" then
set setTarget to "Title and Caption"
else if setTarget is "Title and Caption" then
set setTarget to "Title"
end if
beep
set fmtPrevious to fmtString
set fmtString to ""
end if
end repeat
-- Normalize format string for leading/trailing/extraneous spaces and save for next run. Asterisk -> "•"
set fmtString to do shell script "fmt=$(echo '" & fmtString & "'|xargs); fmt=${fmt//\\*/•}; echo $fmt > ~/.Photos_SetTitle; echo $fmt;"
----------------------------------------
-- Loop on selected media items
--
set selectedMediaItems to selection
repeat with mediaItem in selectedMediaItems
set {TID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, "/"}
set mediaID to mediaItem's id
set mediaID to text item 1 of mediaID -- The database key is the part before the slash.
set dbInfo to do shell script "sqlite3 -readonly -separator '¶' \"" & dbPath & "\" \"SELECT ass.ZFILENAME, ass.ZDIRECTORY, datetime(cast(ass.ZDATECREATED as integer) + 978307200 + cast(att.ZTIMEZONEOFFSET as integer), 'unixepoch'), att.ZTIMEZONENAME, replace(IFNULL(NULLIF(att.ZEXIFTIMESTAMPSTRING, ''), 'NoEXIF NoEXIF '),':','-'), IFNULL(NULLIF(ass.ZLATITUDE, ''), 'No Latitude'), IFNULL(NULLIF(ass.ZLONGITUDE, ''), 'No Longitude'), att.ZORIGINALFILENAME FROM ZASSET ass LEFT JOIN ZADDITIONALASSETATTRIBUTES att ON ass.Z_PK = att.ZASSET WHERE ass.ZUUID = '" & mediaID & "';\""
set AppleScript's text item delimiters to "¶"
set dbFileName to text item 1 of dbInfo
set dbFilePath to "√originals√" & text item 2 of dbInfo & "√" & dbFileName
set dbDate to text 1 through 10 of text item 3 of dbInfo
set dbTime to text 12 through 19 of text item 3 of dbInfo
set dbTZ to text item 4 of dbInfo
set dbDateEXIF to text 1 through 10 of text item 5 of dbInfo
set dbTimeEXIF to text 12 through 19 of text item 5 of dbInfo
set dbLatitude to text item 6 of dbInfo
set dbLongitude to text item 7 of dbInfo
set dbOriginalFileName to text item 8 of dbInfo
set AppleScript's text item delimiters to TID
set mediaMoment to item 1 of (get name of moments whose id of media items contains mediaItem's id)
set mediaCurrentTitle to mediaItem's name
set mediaCurrentCaption to mediaItem's description
set fmtTitle to do shell script "T=" & dbTime & "; t=${T:0:5}; XT=" & dbTimeEXIF & "; XT=${XT//-/:}; Xt=${XT:0:5}; fmt=$(echo '" & fmtString & "'); fmt=${fmt//\\%D/" & dbDate & "}; fmt=${fmt//\\%T/${T}}; fmt=${fmt//\\%t/${t}}; fmt=${fmt//\\%Z/" & dbTZ & "}; fmt=${fmt//\\%XD/" & dbDateEXIF & "}; fmt=${fmt//\\%XT/${XT}}; fmt=${fmt//\\%Xt/${Xt}}; fmt=${fmt//\\%L/" & dbLatitude & "}; fmt=${fmt//\\%l/" & dbLongitude & "}; fmt=${fmt//\\%F/" & dbOriginalFileName & "}; fmt=${fmt//\\%fp/" & dbFilePath & "}; fmt=${fmt//\\%f/" & dbFileName & "}; fmt=${fmt//\\%M/" & mediaMoment & "}; fmt=${fmt//\\%CT/" & mediaCurrentTitle & "}; fmt=${fmt//\\%CC/" & mediaCurrentCaption & "}; echo $fmt"
if setTarget contains "Title" then set mediaItem's name to fmtTitle
if setTarget contains "Caption" then set mediaItem's description to fmtTitle
end repeat
end tell
beep
delay 0.3
beep