×

URL: Omni Links

With “Omni Links” you bookmark files and items (documents, text, graphics, tasks, etc.) for quick access, sharing with others, and later use with other projects.”

Omni Links, a new feature in OmniOutliner 6, are links that navigate to documents created by Omni applications or a specific section within those documents. Unlike standard file URLs, that reference items based upon your device’s unique file hierarchy, Omni Links can be shared with other users and are compatible with any Apple device. This compatibility is enabled by the use of a new “Connected Folders” feature, with which you grant Omni applications explicit access to the folders containing the files you wish to be shared. These “Connected Folders” can even reside on servers accessed by other members of your team.

An Example Use Case

A simple example would be to link to a specific section of an OmniOutliner document containing paragraphs used in the creation of legal instruments (such as “boilerplate” language). You could generate a series of Omni Links pointing to sections of the same “source” document, and by activating those Omni Links, individually retrieve a section instantly for insertion into other documents you are creating. Quick and accurate!

Boilerplate language is just one example. Every profession and field of study has content which is re-used or referenced, whether a graphic element, a formula or chunk of language. Omni Links provides a way to link to such content that works natively across all our apps, across all our devices, and that can be shared with a team. Omni Automation really sings when it gains the ability to create Omni Links and reference those links.

Video 1: “Omni Links” and Networked Teams
A video detailing how to use “Omni Links” with documents stored on a sharepoint server.
OmniOutliner (v6): Share “Omni Links” Plug-In
 

URL Functions for Omni Links:

To enable Omni Automation scripts and plug-ins to generate and interact with Omni Links, the API has been expanded to include the following functions:

Omni Links rely on user-designated “Connected Folders” to provide the location of the targeted file. In this example, the OmniOutliner document (Boilerplate.ooutline) is in the default OmniOutliner Documents folder on the user’s iCloud Drive, which is designated as the Connected Folder:

Example “Omni Link”

omnioutliner:///doc/Documents/Boilerplate.ooutline?folder=iCloud%20Drive

Omni Links may also contain references to specific elements within the host document, such as rows in an OmniOutliner outline. Selected items are represented in the link by their unique IDs used as the value for the “row” parameter:

Example “Omni Link” (element)

omniLinkStr = "omnioutliner:///doc/Documents/Boilerplate.ooutline?folder=iCloud%20Drive&row=bHkiZI2Qms1"

Extracting “Omni Link” Components

Using standard URL class properties and functions, scripts can parse Omni Links to extract its various components.

Document File Name


omniLinkStr = "omnioutliner:///doc/Documents/Boilerplate.ooutline?folder=iCloud%20Drive&row=bHkiZI2Qms1" omniLinkURL = URL.fromString(omniLinkStr) docFileName = omniLinkURL.pathExtension //--> "Boilerplate.ooutline"

To extract the various elements, use the URL.QueryItem class of the URL class:

Connected Folder Name (from URL string)


omniLinkStr = "omnioutliner:///doc/Documents/Boilerplate.ooutline?folder=iCloud%20Drive&row=bHkiZI2Qms1" urlComps = URL.Components.fromString(omniLinkStr) qItem = urlComps.queryItems.find(qItem => qItem.name === "folder") folderName = qItem.value //--> "iCloud Drive"
Connected Folder Name (from URL object)


omniLinkStr = "omnioutliner:///doc/Documents/Boilerplate.ooutline?folder=iCloud%20Drive&row=bHkiZI2Qms1" omniLinkURL = URL.fromString(omniLinkStr) urlComps = URL.Components.fromURL(omniLinkURL, false) qItem = urlComps.queryItems.find(qItem => qItem.name === "folder") folderName = qItem.value //--> "iCloud Drive"

And for OmniOutliner, use the “row” parameter to retrieve the IDs of the selected rows:.

Linked Object Identifiers (OmniOutliner)


omniLinkStr = "omnioutliner:///doc/Documents/Boilerplate.ooutline?folder=iCloud%20Drive&row=bHkiZI2Qms1" urlComps = URL.Components.fromString(omniLinkStr) qItems = urlComps.queryItems.filter(qItem => qItem.name === "row") rowIDs = qItems.map(qItem => qItem.value) //--> ["bHkiZI2Qms1"]

Requesting File URLs from Omni Links

To open a linked document, you generate a file URL to the document using the resolveFileURLForOmniLink(…) function. Since the result of the function is an instance of the Promise class, wrap the script in an asynchronous handler and use the await parameter to wait for the resolution to be completed:

Get a File URL to a Linked Document File


(async () => { try { omniLinkStr = "omnioutliner:///doc/Documents/Boilerplate.ooutline?folder=iCloud%20Drive&row=bHkiZI2Qms1" omniLinkURL = URL.fromString(omniLinkStr) docFileURL = await URL.resolveFileURLForOmniLink(omniLinkURL, null) console.log(docFileURL.absoluteString) //--> file:///Users/otto/Library/Mobile%20Documents/iCloud~com~omnigroup~OmniOutliner/Documents/Boilerplate.ooutline } catch(err){ new Alert(err.name, err.message).show() } })();

As you can see in the example above, use of the absoluteString property reveals the full file path to the linked file.

Interacting with the User

Because Omni Links reply on the user’s approval of Connect Folders, scripts and plug-in requests may trigger dialogs requiring user response. Appropriately, the API includes the ability to inform the user why an interaction is occurring and request a particular action by the user.

For example, here’s a script that is requesting a file URL to a specific document. If the document is not already within a Connected Folder, the user will be prompted to respond:

Get a File URL to a Linked Document File with Justification


omniLinkStr = "omnioutliner:///doc/Documents/Boilerplate.ooutline?folder=iCloud%20Drive&row=bHkiZI2Qms1" omniLinkURL = URL.fromString(omniLinkStr) urlComps = URL.Components.fromURL(omniLinkURL, false) qItem = urlComps.queryItems.find(qItem => qItem.name === "folder") folderName = qItem.value docFileName = omniLinkURL.pathExtension plugInName = "Retrieve Data" reasonForRequest = `in order to process file: ${docFileName}` justification = `The plug-in “${plugInName}” is requesting the registration of folder “${folderName}” ${reasonForRequest}.` docFileURL = await URL.resolveFileURLForOmniLink(omniLinkURL, justification)

Here’s a similar example, showing a script attempting to extract the content of selected rows in an OmniOutliner document:

OmniOutliner: Extracting Content of Linked Rows


(async () => { try { omniLinkStr = "omnioutliner:///doc/Documents/Basic.ooutline?folder=iCloud%20Drive&row=bHkiZI2Qms1" urlComps = URL.Components.fromString(omniLinkStr) qItem = urlComps.queryItems.find(qItem => qItem.name === "folder") folderName = qItem.value docLink = URL.fromString(omniLinkStr) plugInName = "NAME-OF-PLUG-IN" justification = `The plug-in “${plugInName}” is requesting access to this outline in folder “${folderName}”, to make its previously linked content available for use.` fileURL = await URL.resolveFileURLForOmniLink(docLink, justification) console.log("fileURL", fileURL.absoluteString) // OPEN FILE AND PROCESS INDICATED ROWS app.openDocument(document, fileURL, function(doc, wasOpen){ qItems = urlComps.queryItems.filter(qItem => qItem.name === "row") rowIDs = qItems.map(qItem => qItem.value) console.log("rowIDs count:", rowIDs.length) if(rowIDs.length > 0){ for(id of rowIDs){ console.log("id:", id) doc.outline.rootItem.apply(item => { if(item.identifier === id){ console.log(item.topic) return Item.ApplyResult.Stop } }) } } if(!wasOpen){doc.close()} }) } catch(err){ new Alert(err.name, err.message).show() } })();

Requesting an “Omni Link” to a chosen file:

Request an “Omni Link” to Chosen File


(async () => { try { OOFileType = new TypeIdentifier("com.omnigroup.omnioutliner.xmlooutline") picker = new FilePicker() picker.folders = false picker.multiple = false picker.types = [OOFileType] urls = await picker.show() fileURL = urls[0] console.log(fileURL.absoluteString) fileName = fileURL.pathExtension console.log("fileName:", fileName) folderURL = fileURL.deletingLastPathComponent() folderName = folderURL.lastPathComponent console.log("folderName:", folderName) plugInName = "Document LinkMaker" justification = `Plug-In “${plugInName}” is requesting an “Omni Link” for “${fileName}” of folder “${folderName}” to store it for later access.` omniLink = await URL.omniLinkForFileURL(fileURL, null, justification) console.log(omniLink.string) //--> omnioutliner:///doc/Days%20of%20the%20Week.ooutline?folder=Desktop } catch(err){ if(!err.causedByUserCancelling){ new Alert(err.name, err.message).show() } } })();

Here are some of the possible user-facing prompts, triggered by the scripts:

(⬇ see below ) “Omni Link” Request Alert Dialog:

document-link-request-for-chosen-file

Copied Omni Links

Scripts may find it necessary to retrieve Omni Links from the clipboard. Here’s how:

try { if(Pasteboard.general.hasURLs){ urlStr = Pasteboard.general.stringForType(TypeIdentifier.URL) if(urlStr.includes(":///doc/")){ console.log(urlStr) // PROCESSING STATEMENTS GO HERE } else { throw {name:"URL TYPE ISSUE", message:"The copied URL is not an Omni Link object."} } } else { throw {name:"MISSING RESOURCE", message:"The clipboard does not contain a URL object."} } } catch(err){ new Alert(err.name, err.message).show() }
Retrieving “Omni Link” object from Pasteboard
  

try { if(Pasteboard.general.hasURLs){ urlStr = Pasteboard.general.stringForType(TypeIdentifier.URL) if(urlStr.includes(":///doc/")){ console.log(urlStr) // PROCESSING STATEMENTS GO HERE } else { throw {name:"URL TYPE ISSUE", message:"The copied URL is not an Omni Link object."} } } else { throw {name:"MISSING RESOURCE", message:"The clipboard does not contain a URL object."} } } catch(err){ new Alert(err.name, err.message).show() }

Connected Folders

User-designated Connected Folders enable Omni Links to be viable and shareable. Scripts and plug-ins may, on occasion, need to interact with Connect Folders. Here are some typical interactions.

Getting URL of a Connected Folder

To derive the URL of a Connected Folder, include justification in case the folder is not already registered:

(async () => { try { plugInName = "NAME-OF-PLUG-IN" folderName = "NAME-OF-REGISTERED-FOLDER" reasonForRequest = "REASON-FOR-REQUEST" justification = `The plug-in “${plugInName}” is requesting the registration of folder “${folderName}” ${reasonForRequest}.` folderLink = URL.omniLink(".", folderName) folderURL = await URL.resolveFileURLForOmniLink(folderLink, justification) console.log("folderURL", folderURL.absoluteString) // PROCESSING STATEMENTS GO HERE } catch(err){ new Alert(err.name, err.message).show() } })();
Template: Get URL of a Connected Folder
  

(async () => { try { plugInName = "NAME-OF-PLUG-IN" folderName = "NAME-OF-REGISTERED-FOLDER" reasonForRequest = "REASON-FOR-REQUEST" justification = `The plug-in “${plugInName}” is requesting the registration of folder “${folderName}” ${reasonForRequest}.` folderLink = URL.omniLink(".", folderName) folderURL = await URL.resolveFileURLForOmniLink(folderLink, justification) console.log("folderURL", folderURL.absoluteString) // PROCESSING STATEMENTS GO HERE } catch(err){ new Alert(err.name, err.message).show() } })();

(⬇ see below ) Connect Folder Alert Dialog

Folder Registration Alert

Searching a Connected Folder

A common task performed by a script or plug-in would be to search a Connected Folder for a particular file or type of file. Here’s an example of searching a Conneced Folder for PNG image files:

(async () => { try { plugInName = "NAME-OF-PLUG-IN" folderName = "NAME-OF-REGISTERED-FOLDER" reasonForRequest = "REASON-FOR-REQUEST" justification = `The plug-in “${plugInName}” is requesting the registration of folder “${folderName}” ${reasonForRequest}.` folderLink = URL.omniLink(".", folderName) folderURL = await URL.resolveFileURLForOmniLink(folderLink, justification) console.log("folderURL", folderURL.absoluteString) searchFileType = TypeIdentifier.png performRecursiveSearch = false itemURLs = await folderURL.find([searchFileType], performRecursiveSearch) for (url of itemURLs){ console.log(url.string) } itemCount = itemURLs.length hasHave = (itemCount === 1)? "has":"have" itemItems = (itemCount === 1)? "item":"items" alertMsg = `${itemCount} ${itemItems} ${hasHave} been logged to the Console.` new Alert("RESULTS",alertMsg).show() } catch(err){ new Alert(err.name, err.message).show() } })();
Searching a Connected Folder
  

(async () => { try { plugInName = "NAME-OF-PLUG-IN" folderName = "NAME-OF-REGISTERED-FOLDER" reasonForRequest = "REASON-FOR-REQUEST" justification = `The plug-in “${plugInName}” is requesting the registration of folder “${folderName}” ${reasonForRequest}.` folderLink = URL.omniLink(".", folderName) folderURL = await URL.resolveFileURLForOmniLink(folderLink, justification) console.log("folderURL", folderURL.absoluteString) searchFileType = TypeIdentifier.png performRecursiveSearch = false itemURLs = await folderURL.find([searchFileType], performRecursiveSearch) for (url of itemURLs){ console.log(url.string) } itemCount = itemURLs.length hasHave = (itemCount === 1)? "has":"have" itemItems = (itemCount === 1)? "item":"items" alertMsg = `${itemCount} ${itemItems} ${hasHave} been logged to the Console.` new Alert("RESULTS",alertMsg).show() } catch(err){ new Alert(err.name, err.message).show() } })();

Requesting Access to a Specific Folder

If a plug-in or script is designed to automate the designation of a specific folder, such as the user’s Public folder, here’s an example:

(async () => { try { folderLink = URL.omniLink(".", "Public") justification = "The plug-in “Report Summary” is requesting the registration of folder “Public” located in your Home directory, in order to look for and process files shared by team members into that folder." folderURL = await URL.resolveFileURLForOmniLink(folderLink, justification) console.log("folderURL", folderURL.absoluteString) // PROCESSING STATEMENTS GO HERE } catch(err){ if(!err.causedByUserCancelling){ new Alert(err.name, err.message).show() } } })();
Requesting Access to a Specific Folder
  

(async () => { try { folderLink = URL.omniLink(".", "Public") justification = "The plug-in “Report Summary” is requesting the registration of folder “Public” located in your Home directory, in order to look for and process files shared by team members into that folder." folderURL = await URL.resolveFileURLForOmniLink(folderLink, justification) console.log("folderURL", folderURL.absoluteString) // PROCESSING STATEMENTS GO HERE } catch(err){ if(!err.causedByUserCancelling){ new Alert(err.name, err.message).show() } } })();

(⬇ see below ) Requesting Access to a Specific Folder

specific registration folder request

Omni Link for Current Document

A new instance function for the Document class provides the mechanism for script to generate an Omni Link to the current document.

Here’s a basic script for generating an Omni Link for the current document. It includes no optional parameters:

Create an Omni Link for Current Document
  

(async () => { try { omniLink = await document.createOmniLinkURL() console.log(omniLink) } catch(err){ new Alert(err.name, err.message).show() } })(); //--> [object URL: omnioutliner:///doc/Documents/My%20Outline.ooutline?folder=iCloud%20Drive]

Unsaved Document

If the current document has not been previously saved, an error is thrown with the message indicating there is no saved file to reference:

Unsaved Document Error

Not in Connected Folder

If the current document has been saved, but is not residing in a Connected Folder, a prompt similar to this one is displayed to the user, encouraging them to make the document’s container a Connected Folder, or to move the document into an existing Connected Folder:

Document not in connected folder

Should the user choose to Cancel the script execution, an error similar to this one is thrown:

Not in Connected Folder error

Omni Links Plug-Ins

A set of plug-ins for OmniOutliner 6.x that enable the archiving and retrieval of Omni Links.