DevonThink Office Pro notes/minutes to OmniFocus (script)

Hi, just thought this may be useful for others. A script to be added to DevonThink Office Pro that will review your rich text file meeting minutes and will find the actions/projects in the minutes and translates them into OmniFocus new inbox tasks and/or tasks for a specific project.
Also the opportunity to add due- and/or deferdates.
Within the description of the script you’ll find more details.

Since I’m definitely not a trained script writer I have merely re-used and combined all kinds of good stuff from others while adding some of myself. This script is most likely not as good/efficient as it could be, but works for me :)

In case anybody is able to improve/perfect, I would be very grateful!

Hope it is useful for anybody else.

-- Based on original script to automatically scan a text file within DevonThink Pro or DevonThink Pro Office, extract task items and send them to Things

-- Originally written by Luc Beaulieu, Version 1.0, March 7, 2015

-- Re-use portions of a script "Add as To Do to Things" written by Eric Böhnisch-Volkmann, Version 1.0.1, Jan 28, 2010

-- Rik Welter, Oct 2018:
-- Adjusted the script to replace Things by OmniFocus, and in adding extended functionality to also add  defer (DEF:) and/or due (DUE:) dates for the specified action (ACTION:)
-- Also added functionality to set a project (PRJ:) 
-- in case of having set a projectname (e.g. PRJ:xyz) where xyz doesn't exist yet in OmniFocus, the project "xyz" will be created and the tasks added for that project

-- General rule for your minutes/notes document - enter all variables on separate lines, for example:
-- PRJ:
-- ACTION:
-- DUE:
-- DEF:

-- In between those lines you can put any free text you like. However, the order above will remain.
-- This means that if you just want some new inbox tasks, you will not enter PRJ: above those tasks.
-- If you want specific project tasks, first put the PRJ: in a line somewhere above these tasks.
-- If further down you want to have inbox tasks again, you can 'kill' the project functionality by first putting a line in with PRJ:- (just a single dash)
-- and so on.......

-- in order to be able to deal with the dates in an OmniFocus way I have re-used parts of the LATER.SCPT by Chris Sauve of [pxldot](http://pxldot.com).

-- Set properties


property pTags : "DEVONthink"
property timesUsedSinceError : 0

try
	-- Get the selection
	tell application id "com.devon-technologies.thinkpro2" to set thisSelection to the selection
	
	-- Error handling
	
	if thisSelection is {} then error localized string "Please select a document, then try again."
	
	if (length of thisSelection) > 1 then error localized string "Please select only one document, then try again."
	
	-- Get and format the data we need
	
	set pLocalizedTags to localized string of pTags
	
	tell application id "com.devon-technologies.thinkpro2"
		
		set theSource to content record
		
		set theText to plain text of theSource
		
		set theLines to paragraphs of theText
		
		set thisItem to first item of thisSelection
		
		set theSummary to (name of thisItem) as string
		
		-- set theURL to ("[url=x-devonthink-item://" & uuid of thisItem & " " & name of thisItem & "/url]") as string
		set theURL to ("x-devonthink-item://" & uuid of thisItem & " " & name of thisItem) as string
		
	end tell
	
	-- Iterating through each line, one by one, for the string delimeter "ACTION:"
	
	-- and create a new task in Omnifocus global Inbox if appropriate
	
	set textDelim to ""
	set textDelim0 to "PRJ:"
	set textDelim1 to "ACTION:"
	set textDelim2 to "DEF:"
	set textDelim3 to "DUE:"
	set theDuedate to ""
	set theDeferdate to ""
	set theProj to ""
	set newProj to ""
	set theTask to ""
	set finalTask to ""
	
	set nTask to 0
	
	set singleTask to "true"
	
	repeat with eachLine in theLines
		
		set nextLine to eachLine
		
		set finalTask to ""
		
		-- if line contains "PRJ:xyz" all actions following in the note at hand, all actions following will be considered as actions for that project.
		-- Note: in the current version of this script, this will only work when xyz is the exact correct name of a project in Omnifocus
		-- next step in evolving the script will be to research is such that of an unknown project, a new project will be created
		
		if nextLine contains textDelim0 then
			
			set textDelim to textDelim0
			
			set AppleScript's text item delimiters to textDelim
			
			set theTask to item 2 of every text item of nextLine
			
			set AppleScript's text item delimiters to ""
			
			set finalTask to finalTask & theTask
			
			if finalTask is not equal to "" then
				if finalTask is not equal to "-" then
					set theProj to theTask as Unicode text
					set newProj to theProj
				else
					set theProj to ""
				end if
			end if
		end if
		
		-- if line contains "ACTION:xyz" the action will be considered as new inbox action.
		
		if nextLine contains textDelim1 then
			
			
			set textDelim to textDelim1
			
			set AppleScript's text item delimiters to textDelim
			
			set theTask to item 2 of every text item of nextLine
			
			set AppleScript's text item delimiters to ""
			
			set finalTask to finalTask & theTask
			
			if finalTask is not equal to "" then
				
				set nTask to nTask + 1
				
				if theProj is not equal to "" then
					tell application "OmniFocus"
						set task_title to finalTask
						tell default document
							
							if (project theProj exists) then
								set oProj to (first flattened project whose name is my theProj)
								tell oProj
									set theNewtask to make new task with properties {name:task_title}
									set its note to theURL
								end tell
								
							else
								
								if newProj is not equal to "" then
									set theProj to make new project with properties {name:theProj}
								end if
								
								tell theProj
									set theNewtask to make new task with properties {name:task_title}
									set its note to theURL
								end tell
								
								set theProj to newProj as Unicode text
								set newProj to ""
								
							end if
							
						end tell
						
					end tell
					
				else
					
					tell application "OmniFocus"
						set task_title to finalTask
						tell default document
							set newTask to make new inbox task with properties {name:task_title}
							set the note of newTask to theURL
						end tell
						
					end tell
					
				end if
				
			end if
			
		end if
		
		-- if line contains "DEF:xyz" the xyz defer date (starting date) will be added to the new inbox action or new "project action".
		-- part of Omnifocus inteligent date setting is applied -> for example Thursday will translate to dd-mm-yyyy at 00:00am
		
		if nextLine contains textDelim2 then
			
			set textDelim to textDelim2
			
			set AppleScript's text item delimiters to textDelim
			
			set theTask to item 2 of every text item of nextLine
			
			set AppleScript's text item delimiters to ""
			
			set finalTask to finalTask & theTask
			
			if finalTask is not equal to "" then
				set theDeferdate to getDate(theTask)
				tell application "OmniFocus"
					if theProj is not equal to "" then
						set defer date of theNewtask to theDeferdate
					else
						tell front document
							set defer date of newTask to theDeferdate
							
						end tell
					end if
					
				end tell
				
			end if
			
		end if
		
		-- if line contains "DUE:xyz" the xyz due date will be added to the new inbox action or new "project action".
		-- Omnifocus inteligent date setting is applied -> for example Thursday will translate to dd-mm-yyyy at 00:00am, "1d" will translate to tomorrow's date, 1 month to next month's date, etc.
		
		if nextLine contains textDelim3 then
			
			set textDelim to textDelim3
			
			set AppleScript's text item delimiters to textDelim
			
			set theTask to item 2 of every text item of nextLine
			
			set AppleScript's text item delimiters to ""
			
			set finalTask to finalTask & theTask
			
			if finalTask is not equal to "" then
				set theDuedate to getDate(theTask)
				tell application "OmniFocus"
					if theProj is not equal to "" then
						set due date of theNewtask to theDuedate
					else
						tell front document
							set due date of newTask to theDuedate
							
						end tell
					end if
				end tell
			end if
			
		end if
		
	end repeat
	
	
	display dialog (nTask as string) & " were created!"
	
on error errMsg
	
	display alert (localized string "Error extracting task") message errMsg
	
	
	
end try

--//////// Understanding the date and time given in plain english ////////--

on englishTime(dateDesired)
	
	if dateDesired is "0" then return 0
	
	set monthFound to 0
	set weekdayFound to 0
	-- Solves an issue with the treatment of leading zeros for the minutes (i.e., 12:01am)
	set minuteLeadingZero to false
	
	-- Figures out if the user excluded any of the components
	set timeMissing to false
	set daysMissing to false
	set weeksMissing to false
	
	-- Sets up the delimiters for different items
	set timeDelimiters to {"am", "pm", "a", "p", ":"}
	set dayDelimiters to {"days", "day", "d"}
	set weekDelimiters to {"weeks", "week", "w"}
	set monthDelimiters to {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
	set weekdayDelimiters to {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}
	set specialRelativeDayDelimiters to {"Today", "Tomorrow", "at"}
	set otherDelimiters to {" ", "th", "st", "rd", "nd", "start", "due", "both"}
	
	set inThe to "unknown"
	set howManyNumbersInputted to 0
	set numList to {}
	
	-- See if they included AM/PM
	if my isNumberIdentifier("a", dateDesired) then set inThe to "AM"
	if my isNumberIdentifier("p", dateDesired) then set inThe to "PM"
	
	-- See if they gave an absolute date formatted in YY.MM.DD or some other similar format
	set my text item delimiters to specialRelativeDayDelimiters & otherDelimiters & timeDelimiters
	set checkInput to every text item of dateDesired
	set checkInputCleaned to {}
	repeat with i from 1 to (length of checkInput)
		if item i of checkInput is not "" then
			set the end of checkInputCleaned to item i of checkInput
		end if
	end repeat
	set theDateCheck to item 1 of checkInputCleaned
	if (theDateCheck contains ".") or (theDateCheck contains "-") or (theDateCheck contains "/") then
		set todaysDate to (current date)
		set time of todaysDate to 0
		set targetDate to my understandAbsoluteDate(theDateCheck)
		if targetDate = -1 then return -1
		set my text item delimiters to ""
		if length of checkInputCleaned is 1 then
			return (targetDate - todaysDate) as number
		else
			set theTime to items 2 thru -1 of checkInputCleaned
			set numList to {}
			
			set timeStoreLocation to length of theTime
			repeat while timeStoreLocation > 0
				try
					-- If the minutes have a leading zero, just combine them with the hours
					if (numList = {}) and ((item timeStoreLocation of theTime) starts with "0") then
						set the end of numList to ((item (timeStoreLocation - 1) of theTime) & (item timeStoreLocation of theTime)) as number
						set minuteLeadingZero to true
						set timeStoreLocation to timeStoreLocation - 2
					else
						-- Otherwise, get the numbers only
						set tempNum to (item timeStoreLocation of theTime) as number
						if tempNum ≠ 0 then set the end of numList to tempNum
						set timeStoreLocation to timeStoreLocation - 1
					end if
				end try
			end repeat
			
			set theTime to figureOutTheTime(numList, false, true, true, minuteLeadingZero)
			set theTime to understandTheTime(theTime, inThe, false)
			return (targetDate + theTime - todaysDate) as number
		end if
	end if
	
	-- See if they gave an absolute date, a relative one, or a day of the week
	repeat with i from 1 to (length of monthDelimiters)
		if dateDesired contains (item i of monthDelimiters) then
			set monthFound to i
			exit repeat
		end if
		if i ≤ (length of weekdayDelimiters) then
			if dateDesired contains (item i of weekdayDelimiters) then
				set weekdayFound to i
			end if
		end if
	end repeat
	
	-- Getting rid of all the bits I could imagine being around the numbers
	set text item delimiters to (specialRelativeDayDelimiters & monthDelimiters & weekDelimiters & dayDelimiters & timeDelimiters & otherDelimiters)
	set inputList to every text item of dateDesired
	-- Resetting delimiters
	set text item delimiters to {""}
	
	repeat with i from 1 to (length of inputList)
		if item i of inputList is "-" and (character 1 of item (i + 1) of inputList is in "123456789") then
			set item (i + 1) of inputList to item i of inputList & item (i + 1) of inputList
		end if
	end repeat
	
	-- Count how many numbers were given
	repeat with i from 1 to (length of inputList)
		if (item i of inputList) is not "" then
			try
				set tempItem to (item i of inputList) as integer
				if class of tempItem is integer then set howManyNumbersInputted to howManyNumbersInputted + 1
			end try
		end if
		set tempItem to ""
	end repeat
	
	-- Get the numbers of the input — start from the back to get the minutes first
	set timeStoreLocation to length of inputList
	repeat while timeStoreLocation > 0
		try
			-- If the minutes have a leading zero, just combine them with the hours
			if (numList = {}) and ((item timeStoreLocation of inputList) starts with "0") then
				set the end of numList to ((item (timeStoreLocation - 1) of inputList) & (item timeStoreLocation of inputList)) as number
				set minuteLeadingZero to true
				set timeStoreLocation to timeStoreLocation - 2
			else
				-- Otherwise, get the numbers only
				try
					set tempNum to (item timeStoreLocation of inputList) as number
					if tempNum ≠ 0 then set the end of numList to tempNum
				end try
				set timeStoreLocation to timeStoreLocation - 1
			end if
		end try
	end repeat
	
	-- Reverse it so the order is from biggest to smallest time increment
	set numList to reverse of numList
	
	if (monthFound is 0) and (weekdayFound is 0) then
		-- If the user gave a relative date...
		tell dateDesired
			set daysMissing to not my isNumberIdentifier("d", it)
			set weeksMissing to not my isNumberIdentifier("w", it)
			if (howManyNumbersInputted - ((not daysMissing) as integer) - ((not weeksMissing) as integer)) = 0 then set timeMissing to true
		end tell
		
		-- Figure out how many weeks
		if not weeksMissing then
			set weeksDeferred to item 1 of numList
		else
			set weeksDeferred to 0
		end if
		
		-- Figure out how many days
		if not daysMissing then
			set daysDeferred to howManyDays(numList, weeksMissing)
		else
			if dateDesired contains "Tomorrow" then
				-- Special case where they put "tomorrow"
				set daysDeferred to 1
			else
				-- If they exclude it entirely or put "Today"
				set daysDeferred to 0
			end if
		end if
		
		-- Figure out the time
		set timeDeferredTemp to figureOutTheTime(numList, timeMissing, daysMissing, weeksMissing, minuteLeadingZero)
		-- Understand the meaning of the time component
		set timeDeferred to understandTheTime(timeDeferredTemp, inThe, timeMissing)
		
		-- Creating the time deferred based on minutes and hours calculated
		if timeDeferred ≥ 0 then
			set totalTimeDeferred to timeDeferred + daysDeferred * days + weeksDeferred * weeks
		else
			set totalTimeDeferred to timeDeferred
		end if
		-- end of relative date-only code
		
	else if (weekdayFound > 0) and (monthFound is 0) then
		if length of numList < 1 then set timeMissing to true
		-- Same as if the day and the week were missing on a relative date
		set timeDeferredTemp to figureOutTheTime(numList, timeMissing, true, true, minuteLeadingZero)
		set timeDeferred to understandTheTime(timeDeferredTemp, inThe, timeMissing)
		set daysDeferred to daysFromTodayToWeekday(weekdayFound)
		if timeDeferred ≥ 0 then
			set totalTimeDeferred to daysDeferred * days + timeDeferred
		else
			set totalTimeDeferred to timeDeferred
		end if
	else
		-- If the user gave an absolute date...
		if length of numList < 2 then set timeMissing to true
		-- Same as if the day were there but week wasn't on a relative date
		set timeDeferredTemp to figureOutTheTime(numList, timeMissing, false, true, minuteLeadingZero)
		set timeDeferred to understandTheTime(timeDeferredTemp, inThe, timeMissing)
		set timeFromTodayUntilDesired to figuringTimeToDesiredDay(monthFound, (item 1 of numList))
		if timeDeferred ≥ 0 then
			set totalTimeDeferred to timeFromTodayUntilDesired + timeDeferred
		else
			set totalTimeDeferred to timeDeferred
		end if
	end if
	
	return totalTimeDeferred
	
end englishTime


on isNumberIdentifier(possibleIdentifier, containerString)
	set numberIdentifier to true
	set identifierIsInContainer to false
	set positionOfLastIdentifier to 0
	set charList to every character of containerString
	
	repeat with i from 1 to (length of charList)
		if (item i of charList) = possibleIdentifier then
			set identifierIsInContainer to true
			set positionOfLastIdentifier to i
		end if
	end repeat
	
	if (positionOfLastIdentifier is 0) or (positionOfLastIdentifier is 1) then
		set numberIdentifier to false
	else
		set characterBefore to character (positionOfLastIdentifier - 1) of containerString
		set numBefore to 0
		try
			set numBefore to characterBefore as integer
		end try
		if (characterBefore is not " ") and (class of numBefore is not integer) then set numberIdentifier to false
	end if
	return numberIdentifier
end isNumberIdentifier


on howManyDays(numList, weeksMissing)
	if not weeksMissing then
		set daysDeferred to item 2 of numList
	else
		set daysDeferred to item 1 of numList
	end if
	return daysDeferred
end howManyDays


on figureOutTheTime(numList, timeMissing, daysMissing, weeksMissing, minuteLeadingZero)
	if not timeMissing then
		if minuteLeadingZero then
			set timeDeferredTemp to item -1 of numList
		else
			set text item delimiters to ""
			set timeDeferredTemp to ((items -1 thru (1 + ((not daysMissing) as integer) + ¬
				((not weeksMissing) as integer)) of numList) as text) as integer
		end if
	else
		set timeDeferredTemp to 0
	end if
	return timeDeferredTemp
end figureOutTheTime


to understandTheTime(timeDeferredTemp, inThe, timeMissing)
	if timeMissing then
		set timeDeferred to 0
	else
		if timeDeferredTemp > 2400 then
			-- If the time is greater than the 24 hour clock...
			display alert "Please try again: the time you entered was not a valid time of day."
			set timeDeferred to -1
			
		else if timeDeferredTemp = 2400 then
			-- If the time is equal to 2400...
			set timeDeferred to days
			
		else if timeDeferredTemp ≥ 100 then
			-- if they entered the time as a full hour:minute pair (with or without AM/PM and with or without the colon)
			set minutesDeferred to (((characters -2 thru -1 of (timeDeferredTemp as text)) as text) as integer)
			set hoursDeferred to (((characters 1 thru -3 of (timeDeferredTemp as text)) as text) as integer)
			-- Figuring out the minutes and hours in the time given (minutes are last two numbers)
			
			if inThe = "PM" then
				-- For any number specifically designated as PM
				set timeDeferred to ((hoursDeferred + 12) * hours + minutesDeferred * minutes)
			else if hoursDeferred = 12 and inThe = "AM" then
				-- For 12:00AM exactly
				set timeDeferred to minutesDeferred * minutes
			else
				-- For times in the AM (implicit or explicit) and explicit times in the PM (i.e., 16:00)
				set timeDeferred to (hoursDeferred * hours + minutesDeferred * minutes)
			end if
			
		else if timeDeferredTemp > 24 then
			-- If they entered the time as a single number above 24
			display alert "Please try again: the time you entered was not a valid time of day."
			set timeDeferred to -1
			
		else if timeDeferredTemp ≤ 24 then
			-- If the entered the time as a single number (with or without AM/PM)	
			if timeDeferredTemp = 24 then
				-- If they entered 24 hours exactly (treat as a full extra delay)
				set timeDeferred to days
			else if (timeDeferredTemp = 12) and (inThe ≠ "AM") then
				-- If they entered "12" (treat it as 12PM)
				set timeDeferred to 12 * hours
			else if (timeDeferredTemp ≥ 12) or (inThe ≠ "PM") then
				-- For implicit and explicit AM entries and for implicit PM entries
				set timeDeferred to timeDeferredTemp * hours
			else
				-- For explicit PM entries
				set timeDeferred to (timeDeferredTemp + 12) * hours
			end if
		end if
	end if
	return timeDeferred
end understandTheTime


to figuringTimeToDesiredDay(monthDesired, dayDesired)
	set todaysDate to (current date)
	set time of todaysDate to 0
	-- Creating an intial date object
	copy todaysDate to exactDesiredDate
	set (day of exactDesiredDate) to dayDesired
	set (month of exactDesiredDate) to monthDesired
	if exactDesiredDate < (current date) then
		set (year of exactDesiredDate) to ((year of todaysDate) + 1)
	end if
	return (exactDesiredDate - todaysDate)
end figuringTimeToDesiredDay


on daysFromTodayToWeekday(weekdayDesired)
	set currentWeekday to (weekday of (current date)) as integer
	if currentWeekday = weekdayDesired then
		set daysDeferred to 0
	else if currentWeekday < weekdayDesired then
		set daysDeferred to weekdayDesired - currentWeekday
	else
		set daysDeferred to 7 + weekdayDesired - currentWeekday
	end if
	return daysDeferred
end daysFromTodayToWeekday

on understandAbsoluteDate(theText)
	set theDate to (current date)
	set the day of theDate to 1
	set the month of theDate to 2
	set theDate to (theDate - 1 * days)
	set theDate to short date string of theDate
	
	set text item delimiters to {".", "-", "/", "–", "—", "|", "\\"}
	set theDate to every text item of theDate
	set thePositions to {theDay:0, theMonth:0, theYear:0}
	
	-- Checks the positions of the date components based on January 31 of this year
	repeat with i from 1 to (length of theDate)
		tell item i of theDate
			if it is in "01" then
				set (theMonth in thePositions) to i
			else if it is in "31" then
				set (theDay in thePositions) to i
			else
				set (theYear in thePositions) to i
			end if
		end tell
	end repeat
	
	set theText to every text item of theText
	
	set targetDate to (current date)
	set time of targetDate to 0
	if (length of theText is not 2) and (length of theText is not 3) then
		-- If they don't input at 2-3 numbers, return the error
		return -1
	else
		if length of theText is 3 then
			-- If the input has three numbers
			set the year of targetDate to my solveTheYear((item (theYear of thePositions) of theText) as number)
		else
			-- If the input has two numbers (left out the year)
			set thePositions to my adjustPositionsForNoYear(thePositions)
		end if
		set the month of targetDate to (item (theMonth of thePositions) of theText) as number
		set the day of targetDate to (item (theDay of thePositions) of theText) as number
		-- if targetDate is less than (current date) then
		-- set the year of targetDate to (the year of (current date)) + 1 <-- this original line puts a date in the past to the required date + 1 year. Rik Welter: I have removed this. For me that is better because it will put tasks immediately available in my perspectives with a date in the past, hence identifying errors in notes made, and adjust the task appropriately		
		-- end if
	end if
	return targetDate
end understandAbsoluteDate

to adjustPositionsForNoYear(thePositions)
	if (theYear in thePositions) is 1 then
		set (theMonth in thePositions) to (theMonth in thePositions) - 1
		set (theDay in thePositions) to (theDay in thePositions) - 1
	else if yearPosition is 2 then
		if (theDay in thePositions) < (theMonth in thePositions) then
			set (theMonth in thePositions) to (theMonth in thePositions) - 1
		else
			set (theDay in thePositions) to (theDay in thePositions) - 1
		end if
	end if
	return thePositions
end adjustPositionsForNoYear

to solveTheYear(num)
	if num ≥ 1000 then
		return num
	else
		return (2000 + num)
	end if
end solveTheYear

to getDate(theTask)
	
	-- Setting the desired date based on input
	set desiredDate to (current date)
	set time of desiredDate to 0
	set secondsDeferred to my englishTime(theTask)
	if secondsDeferred = -1 then
		set timesUsedSinceError to 0
		return -1
	else
		set timesUsedSinceError to timesUsedSinceError + 1
	end if
	return desiredDate + secondsDeferred
end getDate
1 Like

While using the script I found an error in it: when adding two actions in a row for a non existing project that had to be created, the script failed.

I’ve solved it and updated the script in the first post.

And to make it all work best for me I’ve thrown Drafts in the pool as well: Created an actiongroup which helps me set a first meeting notes template, plus actions that will quickly generate the delimiters I need for the above script.
After minutes done om my iPad, I use a drafts action to send it to DevonThink, en in DevonThink I run the script to get all the actions and dates automatically added to OmniFocus.

Thanks.