Task Attachments

A task attachment is a representation for a file that is added (“attached”) to a task. Omni Automation in OmniFocus provides the ability to add, retrieve, and remove attachments to/from tasks.

Task Attachment Properties

There is one property regarding file attachments for the Task class:

Task Attachment Functions

The functions of the Task class dealing with the management of attachments.

Adding Attachments

Task attachments are instances of the FileWrapper class with each instance representing a file. FileWrappers can be generated from data of various FileTypes, such as images, and text, property list, and document files.

The following example plug-in demonstrates how create FileWrapper instances from chosen files and “attach” them to a selected task.

Add Chosen Files to Task as Attachments
  

/*{ "type": "action", "targets": ["omnifocus"], "author": "Otto Automaator", "identifier": "com.omni-automation.of.add-attachments-to-task", "version": "1.2", "description": "This action will add chosen files as attachments to the selected task.", "label": "Add Attachments to Task", "shortLabel": "Add Attachments", "paletteLabel": "Add Attachments", "image": "paperclip.badge.ellipsis" }*/ (() => { const action = new PlugIn.Action(async function(selection, sender){ task = selection.tasks[0] picker = new FilePicker() picker.folders = false picker.multiple = true picker.types = null // any file type urlsArray = await picker.show() urlsArray.forEach(url =>{ urlStr = url.string fileName = urlStr.substring(urlStr.lastIndexOf('/') + 1) url.fetch(function(data){ wrapper = FileWrapper.withContents(decodeURIComponent(fileName), data) task.addAttachment(wrapper) }) }) }); action.validate = function(selection, sender){ return (selection.tasks.length === 1) }; return action; })();

The following script creates a file attachment in JSON format to the selected task. Although there is no corresponding file on disk, the created attachment can be used to the store data passed to it.

(The following pair of JSON attachment scripts inspired by a plug-in example at graypegg.com)
omnifocus://localhost/omnijs-run?script=var%20tasks%20%3D%20document%2Ewindows%5B0%5D%2Eselection%2Etasks%0Aif%28tasks%2Elength%20%3D%3D%3D%201%29%7B%0A%09var%20%C6%92names%20%3D%20tasks%5B0%5D%2Eattachments%2Emap%28wrapper%20%3D%3E%20wrapper%2EpreferredFilename%29%0A%09var%20attachmentName%20%3D%20%22Storage%2Ejson%22%0A%09if%28%C6%92names%2Eincludes%28attachmentName%29%29%7B%0A%09%09console%2Eerror%28%22Existing%20Attachment%3A%22%2C%20attachmentName%29%0A%09%7D%20else%20%7B%0A%09%09var%20obj%20%3D%20%7B%22firstName%22%3A%22Otto%22%2C%22lastName%22%3A%22Automator%22%7D%0A%09%09var%20json%20%3D%20JSON%2Estringify%28obj%29%0A%09%09var%20attachmentData%20%3D%20Data%2EfromString%28json%29%0A%09%09var%20wrapper%20%3D%20FileWrapper%2EwithContents%28attachmentName%2C%20attachmentData%29%0A%09%09tasks%5B0%5D%2EaddAttachment%28wrapper%29%09%0A%09%7D%0A%7D
Create JSON File Attachment
 

tasks = document.windows[0].selection.tasks if(tasks.length === 1){ ƒnames = tasks[0].attachments.map(wrapper => wrapper.preferredFilename) attachmentName = "Storage.json" if(ƒnames.includes(attachmentName)){ console.error("Existing Attachment:", attachmentName) } else { obj = {"firstName":"Otto","lastName":"Automator"} json = JSON.stringify(obj) attachmentData = Data.fromString(json) wrapper = FileWrapper.withContents(attachmentName, attachmentData) tasks[0].addAttachment(wrapper) } }

And here’s a companion script for reading the contents of the created JSON file attachment:

omnifocus://localhost/omnijs-run?script=var%20tasks%20%3D%20document%2Ewindows%5B0%5D%2Eselection%2Etasks%0Aif%28tasks%2Elength%20%3D%3D%3D%201%29%7B%0A%09var%20attachmentName%20%3D%20%22Storage%2Ejson%22%0A%09var%20storedData%20%3D%20null%0A%09var%20wrappers%20%3D%20tasks%5B0%5D%2Eattachments%0A%09for%20%28var%20i%20%3D%200%3B%20i%20%3C%20wrappers%2Elength%3B%20i%2B%2B%29%7B%20%0A%20%20%09%09if%20%28wrappers%5Bi%5D%2EpreferredFilename%20%3D%3D%3D%20attachmentName%29%7B%0A%20%20%09%09%09var%20storedData%20%3D%20wrappers%5Bi%5D%2Econtents%2EtoString%28%29%0A%20%20%09%09%09break%0A%20%20%09%09%7D%0A%09%7D%0A%09if%28storedData%29%7B%0A%09%09console%2Elog%28storedData%29%0A%09%09var%20json%20%3D%20JSON%2Eparse%28storedData%29%0A%09%09%2F%2F%20processing%20statements%0A%09%7D%20else%20%7B%0A%09%09console%2Eerror%28%22Missing%20Attachment%3A%22%2C%20attachmentName%29%0A%09%7D%0A%7D
Read Data of JSON Attachment
 

tasks = document.windows[0].selection.tasks if(tasks.length === 1){ attachmentName = "Storage.json" storedData = null wrappers = tasks[0].attachments for (var i = 0; i < wrappers.length; i++){ if (wrappers[i].preferredFilename === attachmentName){ var storedData = wrappers[i].contents.toString() break } } if(storedData){ console.log(storedData) json = JSON.parse(storedData) // processing statements } else { console.error("Missing Attachment:", attachmentName) } }

Deleting Attachments

Should you wish to remove attachments from their assigned task, Omni Automation provides two mechanisms for deleting existing task attachments:

omnifocus://localhost/omnijs-run?script=try%7Bvar%20tasks%20%3D%20document%2Ewindows%5B0%5D%2Eselection%2Etasks%0Atasks%2EforEach%28task%20%3D%3E%7B%0A%09task%2Eattachments%20%3D%20%5B%5D%0A%7D%29%7Dcatch%28err%29%7Bconsole%2Elog%28err%29%7D
Deleting All Attachments of Selected Tasks
 

tasks = document.windows[0].selection.tasks tasks.forEach(task =>{ task.attachments = [] })

To delete specific attachments, use the removeAttachmentAtIndex(…) function combined with comparisons of the values of FileWrapper properties of each attachment.

For example, here's a script that will remove just the image attachments from the selected task:

omnifocus://localhost/omnijs-run?script=try%20%7B%0A%09var%20sel%20%3D%20document%2Ewindows%5B0%5D%2Eselection%0A%09var%20selCount%20%3D%20sel%2Etasks%2Elength%20%2B%20sel%2Eprojects%2Elength%20%0A%09if%28selCount%20%3D%3D%3D%201%29%7B%0A%09%09if%20%28sel%2Etasks%2Elength%20%3D%3D%3D%201%29%7B%0A%09%09%09var%20selectedItem%20%3D%20sel%2Etasks%5B0%5D%0A%09%09%7D%20else%20%7B%0A%09%09%09var%20selectedItem%20%3D%20sel%2Eprojects%5B0%5D%0A%09%09%7D%0A%09%7D%20else%20%7B%0A%09%09throw%20%7B%0A%09%09%09name%3A%20%22Selection%20Issue%22%2C%20%0A%09%09%09message%3A%20%22Please%20select%20a%20single%20project%20or%20task%2E%22%0A%09%09%7D%0A%09%7D%0A%09%0A%09var%20wrappers%20%3D%20selectedItem%2Eattachments%0A%09for%20%28var%20i%20%3D%20wrappers%2Elength%20%2D%201%3B%20i%20%3E%3D%200%3B%20i%2D%2D%29%7B%0A%09%09var%20wrapper%20%3D%20wrappers%5Bi%5D%0A%09%09if%20%28%0A%09%09%09wrapper%2Etype%20%3D%3D%3D%20FileWrapper%2EType%2EFile%20%26%26%0A%09%09%09%28%0A%09%09%09%09wrapper%2Efilename%2EtoUpperCase%28%29%2EendsWith%28%22%2EJPG%22%29%20%7C%7C%20%0A%09%09%09%09wrapper%2Efilename%2EtoUpperCase%28%29%2EendsWith%28%22%2EJPEG%22%29%20%7C%7C%20%0A%09%09%09%09wrapper%2Efilename%2EtoUpperCase%28%29%2EendsWith%28%22%2EPNG%22%29%20%7C%7C%0A%09%09%09%09wrapper%2Efilename%2EtoUpperCase%28%29%2EendsWith%28%22%2ETIFF%22%29%20%7C%7C%0A%09%09%09%09wrapper%2Efilename%2EtoUpperCase%28%29%2EendsWith%28%22%2ETIF%22%29%0A%09%09%09%29%0A%09%09%29%0A%09%09%7B%0A%09%09%09selectedItem%2EremoveAttachmentAtIndex%28i%29%0A%09%09%7D%0A%09%7D%0A%7D%0Acatch%28err%29%7B%0A%09new%20Alert%28err%2Ename%2C%20err%2Emessage%29%2Eshow%28%29%0A%7D
Remove Image Attachments
 

try { sel = document.windows[0].selection selCount = sel.tasks.length + sel.projects.length if(selCount === 1){ if (sel.tasks.length === 1){ var selectedItem = sel.tasks[0] } else { var selectedItem = sel.projects[0] } } else { throw { name: "Selection Issue", message: "Please select a single project or task." } } wrappers = selectedItem.attachments for (var i = wrappers.length - 1; i >= 0; i--){ var wrapper = wrappers[i] if ( wrapper.type === FileWrapper.Type.File && ( wrapper.filename.toUpperCase().endsWith(".JPG") || wrapper.filename.toUpperCase().endsWith(".JPEG") || wrapper.filename.toUpperCase().endsWith(".PNG") || wrapper.filename.toUpperCase().endsWith(".TIFF") || wrapper.filename.toUpperCase().endsWith(".TIF") ) ) { selectedItem.removeAttachmentAtIndex(i) } } } catch(err){ new Alert(err.name, err.message).show() }

NOTE: the previous example uses a JavaScript for loop to iterate the list of attachments in reverse order, removing only those file attachments whose file name ends with specified file extensions.

Export All Attachments

The following plug-in will export all of the attachments of the selected tasks into a new folder placed within a chosen directory.

Export All Attachments of Selected Tasks
  

/*{ "type": "action", "targets": ["omnifocus"], "author": "Otto Automator", "identifier": "com.omni-automation.of.export-attachments-of-selected-tasks", "version": "1.1", "description": "This action will export all of the attachments of the selected tasks into a new folder placed in a user-chosen directory.", "label": "Export Attachments of Selected Tasks", "shortLabel": "Export Attachments", "paletteLabel": "Export Attachments", "image": "square.and.arrow.up.circle.fill" }*/ (() => { const action = new PlugIn.Action(async function(selection, sender){ tasks = selection.tasks fileWrappers = new Array() tasks.forEach(task =>{ if(task.attachments.length > 0){ fileWrappers = fileWrappers.concat(task.attachments) } }) if (fileWrappers.length === 0){throw new Error("No attachments")} filesaver = new FileSaver() folderType = new FileType("public.folder") filesaver.types = [folderType] fldrwrapper = FileWrapper.withChildren("Export Folder", fileWrappers) urlObj = await filesaver.show(fldrwrapper) console.log(urlObj.string) urlObj.open() }); action.validate = function(selection, sender){ return (selection.tasks.length > 0) }; return action; })();
 

Project/Task Documentation in Markdown Format

Here’s an example of using attachments to store, retrieve, and edit documentation in Markdown format for a selected task or project.

A set of Omni Automation plug-ins perform the following procedures:

TIP: qlmarkdown is a QuickLook plug-in from Phil Toland that when installed, enables the viewing of Markdown files in macOS using QuickLook. The following video demonstrates its use.

DOWNLOAD a ZIP archive of a folder containing the plug-ins. To install, place the folder containing the plug-ins in one of the plug-in folders designated in the OmniFocus Automation Configuration dialog. When installed, the plug-ins will appear as a sub-menu in the OmniFocus Automation menu.

Plug-Ins Sub-Menu

TIP: For easy access, add the plug-ins to the macOS OmniFocus toolbar.

Plug-Ins in OmniFocus Toolbar