OF2OPML from Rob Trew

Enclosed below is the text for Rob Trew’s OF2OPML script. This Applescript exports some or all of your OmniFocus database to OPML, a widely accepted outline interchange format. OmniOutliner reads and writes OPML, as do most other outliners and mindmappers.

This version works as of this afternoon with OF2.5 running on Yosemite.

Some Caveats:

If you select nothing, it will export all your projects and their actions to an OPML file.

If you select one or more Project headlines in the main list window, it will confine the export to the selected projects.

It exports only the fields that contain data. If none of your tasks have durations, it won’t export the duration field. This may cause problems later if you want to edit or add new tasks in OmniOultiner. As a solution, I add a dummy project with a single dummy task and fill out all its fields.

It takes a while, and it doesn’t give any indication of its progress. When it’s done, it will prompt for a location to save the OPML file.

My current workflow is to export projects to OPML, store and augment them in OmniOutliner. When I need to move something back into OF2.5, I copy them into a new Outline, and import that document into OF2.5.

Here is the script:

-- Indicative draft Ver 4.00 - OPTION TO DUMP ALL ACTIVE FOLDERS AND PROJECTS - (Currently slow)

-- Removed space before header, and added space after attributes, 
-- for better compatibility with other applications
-- Saves anything selected in Omnifocus (Project or Context View) As an OPML
-- Including the following fields: DONE, NOTE, CONTEXT, PROJECT, START, DUE, COMPLETED, DURATION, FLAGGED}
-- Note that the whole sub-tree is copied, so only 'parent' elements need to be selected.

property pManualSelection : true

property pPROJECT : "project"
property pTASK : "task"
property pINBX_TASK : "inbox task"
property pITEM : "item"

property pOPMLHeadToExpand : "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<opml version=\"1.0\">
	<head>
	<title>Selected in OF</title>
	<expansionState>"
property pOPMLHeadFromExpand : "</expansionState>
	</head>
	<body>
 "

property pOPMLTail : "
	</body>
</opml>"

property pNodeStart : "<outline "
property pLeafClose : "/>"
property pParentClose : "</outline>"

on run
	
	set {lstExport, blnContext} to SelectedInOF(pManualSelection)
	
	set strOPML to MakeOPML({lstExport, blnContext})
	if strOPML ≠ "" then
		set oFile to choose file name with prompt "Save as OPML" default name "Untitled.opml" default location (path to desktop) as alias
		WriteText2Path(strOPML, POSIX path of oFile)
	end if
end run

-- READ SELECTED OmniFocus CONTENT TREE(S) TO NESTED APPLESCRIPT LISTS - Ver.04
on SelectedInOF(pManualSelection)
	tell application "OmniFocus"
		if pManualSelection then
			tell front window
				set blnContext to ((«class FCvm») is not equal to pPROJECT)
				
				repeat with oPanel in {content, sidebar}
					set lstNodes to value of (selected trees of oPanel where class of its value ≠ item)
					set lngNodes to count of lstNodes
					if lngNodes > 0 then exit repeat
				end repeat
				if (lngNodes < 1) then set lstNodes to value of (trees of content where class of its value ≠ item)
			end tell
		else
			tell default document to set lstNodes to ((projects where status is active) as list) & (folders where hidden is false) as list
			set blnContext to false
		end if
		
		repeat with i from 1 to length of lstNodes
			tell item i of lstNodes
				if (its class) is not folder then
					if (number of tasks) > 0 then
						--set item i of lstNodes to {name, completed, my ListSubNodes(its tasks, blnContext, blnAll), note, "", "", start date, due date, completion date, estimated minutes, flagged}
						set item i of lstNodes to {name, completed, my ListSubNodes(its tasks, blnContext), note, "", "", defer date, due date, completion date, estimated minutes, flagged}
					else
						set item i of lstNodes to {name, completed, {}, note, "", "", defer date, due date, completion date, estimated minutes, flagged}
					end if
				else
					if (number of projects) > 0 then
						set item i of lstNodes to {name, false, my ListSubNodes(its projects, blnContext), note, "", "", missing value, missing value, missing value, missing value, false}
					else
						set item i of lstNodes to {name, false, {}, note, "", "", missing value, missing value, missing value, missing value, false}
					end if
				end if
			end tell
		end repeat
		
		return {lstNodes, blnContext}
	end tell
end SelectedInOF

on ListSubNodes(lstNodes)
	using terms from application "OmniFocus"
		repeat with i from 1 to length of lstNodes
			tell item i of lstNodes
				
				set oProj to its containing project
				if oProj is not missing value then
					set strProject to name of oProj
				else
					set strProject to ""
				end if
				
				set oContext to its context
				if oContext is not missing value then
					set strContext to name of oContext
				else
					set strContext to ""
				end if
				
				if (number of tasks) > 0 then
					set item i of lstNodes to {name, completed, my ListSubNodes(its tasks), note, strProject, strContext, defer date, due date, completion date, estimated minutes, flagged}
				else
					set item i of lstNodes to {name, completed, {}, note, strProject, strContext, defer date, due date, completion date, estimated minutes, flagged}
				end if
			end tell
		end repeat
		return lstNodes
	end using terms from
end ListSubNodes


-- BUILD OPML

on MakeOPML({lstTasks, blnContext})
	if (length of lstTasks > 0) then
		
		set {lngIndex, strExpand, strOutline} to my Tasks2OPML(-1, lstTasks, tab)
		set strOPML to pOPMLHeadToExpand & strExpand & pOPMLHeadFromExpand & strOutline & pOPMLTail
		return strOPML
	end if
end MakeOPML

on Tasks2OPML(lngIndex, lstTasks, strIndent)
	set {strExpand, strOut} to {"", ""}
	repeat with oTask in lstTasks
		set {strName, blnDone, lstChiln, strNote, strProject, strContext, dteStart, dteDue, dteDone, lngMins, blnFlagged} to oTask
		
		if strNote ≠ "" then
			set strOut to strOut & pNodeStart & Attr("text", strName) & Attr("_note", strNote)
		else
			set strOut to strOut & pNodeStart & Attr("text", strName)
		end if
		
		if blnDone then if (dteDone is not missing value) then
			set strOut to strOut & Attr("_status", "checked") & Attr("Completed", short date string of dteDone & space & time string of dteDone)
		end if
		
		if strProject ≠ "" then set strOut to strOut & Attr("Project", strProject)
		if strContext ≠ "" then set strOut to strOut & Attr("Context", strContext)
		
		tell dteStart to if it is not missing value then set strOut to strOut & my Attr("Start", short date string & space & time string)
		tell dteDue to if it is not missing value then set strOut to strOut & my Attr("Due", short date string & space & time string)
		
		if lngMins > 0 then set strOut to strOut & Attr("Duration", ((lngMins / 60) as string) & "h")
		if blnFlagged then set strOut to strOut & Attr("Flagged", "2")
		
		set lngIndex to lngIndex + 1
		if (length of lstChiln > 0) then
			set strExpand to strExpand & "," & (lngIndex) as string
			set {lngIndex, strSubExpand, strSubOutln} to Tasks2OPML(lngIndex, lstChiln, strIndent & tab)
			if strSubExpand ≠ "" then set strExpand to strExpand & "," & strSubExpand
			set strOut to strOut & ">" & return & ¬
				strIndent & strSubOutln & return & ¬
				strIndent & pParentClose
		else
			set strOut to strOut & pLeafClose & return
		end if
	end repeat
	if strExpand begins with "," and length of strExpand > 1 then set strExpand to text 2 thru -1 of strExpand
	return {lngIndex, strExpand, strOut}
end Tasks2OPML

on Attr(strName, strValue)
	strName & "=" & EscapeChars(strValue) & space
end Attr

on EscapeChars(str)
	-- QUOTE < > & ETC
	set strEncoded to (do shell script "python -c 'import sys; from xml.sax.saxutils import quoteattr; print quoteattr(sys.argv[1])' " & ¬
		quoted form of str)
	
	-- ENCODE DIACRITICS AND SPECIAL CHARACTERS
	set lstChars to characters of strEncoded
	repeat with i from 1 to length of lstChars
		set lngCode to id of item i of lstChars
		if lngCode > 127 then set item i of lstChars to ("&#" & lngCode as string) & ";"
	end repeat
	lstChars as Unicode text
end EscapeChars

on WriteText2Path(strText, strPosixPath)
	set f to (POSIX file strPosixPath)
	open for access f with write permission
	write strText as «class utf8» to f
	close access f
end WriteText2Path
1 Like

Could you explain a bit about your workflow? (Or maybe direct me to where you’ve written about it in the past.) I’m curious how you use this script to help your task planning/organizing. Thanks!

My database has become bloated with future planning. I can’t just throw it out, so I need to manage it.

My solution (given the various bugs and limitations in OF2.5) is to offload future/inactive projects to OmniOutliner via the OPML export script. Later, when I want to activate one of the future projects, I can copy it into a blank outline, and then import that outline into OF2.5 using the Open OmniOutliner Document command in the File Menu.

1 Like

You’ve clearly given this a lot of thought, so I’m wondering what the advantages are to exporting to OO/OPML versus moving items to a separate OF document. Thanks.

I found that Copy/paste and drag/drop between documents is broken in 2.5, so using OmniOutliner is my workaround.

Thanks so much, @anamorph

Wow. This script is a godsend. Thank you!

OmniFocus to OPML script from Rob Trew malfunctions whenever it takes more than a few moments to complete.
Do you know why this error?
Is there a way I can fix this, or is there a new and improved way to export and archive my OF projects?
Ideally, I want to integrate my OF with Devonthink Pro Office. That would be sweet!

Thanks!
Joseph Roberson