Hi, my main 2 Apps on the Mac are Devonthink (4 Beta2) and Omnifocus 4. I switched to writing all my notes of meetings, thoughts, … in markdown in Devonthink. I use tasks “- ” Syntax for ToDo Items. I wanted an automated script to get all the Tasks of a markdown file as:
- Single ToDos in Omnifocus.
- Enter a backlink to Devonthink in the note section of Omnifocus task
- make a (OF) entry at the end of the task in Devonthink markdown file, so I know if it is already sent to Omnifocus
- Don’t make another entry for these marked Tasks in Omnifocus when running the script again.
Didn’t get it on the first try, but thanks to ChatGPT the Applescript is now working.
Anybody interested in the script?
You are free to post it, if you feel it’s beneficial.
You are of course free to post scripts. But in my experience, AI generated AppleScript code is badly written and convoluted. It may do what it should do, but it’s often not a good example for people to learn how to code.
But if it’s working, then it’s a useful draft for people familiar with AppleScript and saving time. AI has its pros and cons, like everything in life 
Maybe it is useful for someone…
How do I post a script file in the forum?
Just add the source code to your post and enclose it in ```
Here is the Applescript:
-- AppleScript: DEVONthink 4 → OmniFocus 4 (robust, detects indented tasks)
-- Helper: Remove leading spaces and tabs
on trimLeadingSpaces(t)
do shell script "echo " & quoted form of t & " | sed -E 's/^[[:space:]]*//'"
end trimLeadingSpaces
-- Helper: Extract task text after "- [ ]"
on extractTaskText(lineText)
do shell script "echo " & quoted form of lineText & " | sed -E 's/^[[:space:]]*- \\[ \\] ?//'"
end extractTaskText
-- 1. Get the currently selected Markdown document from DEVONthink
tell application "DEVONthink"
set theSelection to the selection
if theSelection is {} then
display dialog "No document selected in DEVONthink." buttons {"OK"} default button "OK"
return
end if
set theRecord to item 1 of theSelection
set theText to get plain text of theRecord
set theLink to reference URL of theRecord
end tell
-- 2. Collect lines that are tasks (start with - [ ] and not already sent to OmniFocus)
set taskList to {}
set originalLines to paragraphs of theText
repeat with aLine in originalLines
set trimmedLine to my trimLeadingSpaces(aLine)
if trimmedLine starts with "- [ ]" and aLine does not contain "(OF)" then
set end of taskList to aLine
end if
end repeat
if (count of taskList) = 0 then
display dialog "No new tasks found." buttons {"OK"} default button "OK"
return
end if
-- 3. Create tasks in OmniFocus and mark lines in Markdown
set updatedLines to originalLines
tell application "OmniFocus"
tell default document
repeat with t in taskList
-- Extract the clean task text from Markdown
set taskName to my extractTaskText(t)
-- Create a new inbox task with the document link
set newTask to make new inbox task with properties {name:taskName, note:"Link to DEVONthink document: " & theLink}
-- Mark the task line with (OF) in the Markdown
repeat with i from 1 to count of updatedLines
set currentLine to item i of updatedLines
set trimmedCurrent to my trimLeadingSpaces(currentLine)
set currentContent to my extractTaskText(trimmedCurrent)
if currentContent is equal to taskName and currentLine does not contain "(OF)" then
set item i of updatedLines to currentLine & " (OF)"
exit repeat
end if
end repeat
end repeat
end tell
end tell
-- 4. Write updated Markdown content back to DEVONthink
set AppleScript's text item delimiters to linefeed
set finalText to updatedLines as string
set AppleScript's text item delimiters to ""
tell application "DEVONthink"
set plain text of theRecord to finalText
end tell
display dialog "Tasks have been created in OmniFocus and marked in the Markdown document." buttons {"OK"} default button "OK"
2 Likes
As expected…
For example:
- every line of the text is trimmed
- Then every line is checked for the task markers at the start of it
The first step were not needed if one would simply check for optional space at beginning of line followed by the task marker
The lines are then trimmed again later, btw, in the disguise of updatedLines
.
Again: I’m not saying that the code is not working. It’s just overly contrived and difficult to understand.
1 Like
I had the problem that in my Code only Spaces were found but no tabs… that was where I started asking AI for help….
Here is a non-AI, human-written, pure AppleScript with the core function of getting the tasks from the document and passing them to a handler where you can do whatever with them…
tell application id "DNtp"
if (selected records) is {} then return
repeat with theRecord in (selected records)
if (record type of theRecord is markdown) then
set src to (plain text of theRecord)
set recordID to (reference URL of theRecord)
set od to AppleScript's text item delimiters
set theTasks to {}
repeat with theParagraph in (paragraphs of src)
if theParagraph contains "- [ ]" then
set AppleScript's text item delimiters to "[ ] "
set theTask to text item -1 of (text items of theParagraph)
if theTask is not "" then copy {|name|:theTask, |note|:"Link to DEVONthink document: ", |URL|:recordID} to end of theTasks
end if
end repeat
set AppleScript's text item delimiters to od
if theTasks is not {} then
my processtasks(theTasks)
else
log message "No tasks were found in this document." record theRecord
end if
end if
end repeat
end tell
on processtasks(theTasks)
repeat with theTask in theTasks
display alert "" & theTask's |name| & return & theTask's |note| & return & theTask's |URL|
end repeat
end processtasks
PS: This is DEVONthink 4 using record type
. type
would work in DEVONthink 3.
Quick example
Here is a simple practical use of the handler using Cultured Code’s Things…
on processtasks(theTasks)
tell application "Things3"
repeat with theTask in theTasks
make new to do with properties {name:theTask's |name|, notes:(|note| of theTask & theTask's |url|) as string}
end repeat
end tell
end processtasks
1 Like
Thanks, that code looks a lot nicer…
I tried the script with this markdown Document
## Testing again
Hier ist der normale Text
- [ ] Eine Aufgabe zum testen
It returns “No tasks were found in this document”
There’s a typo in the script… Do you see it? 
repeat with theParagraph in (paragraphs of src)
if theParagraph contains "-[ ]" then
Thanks. Script adjusted. I added the -
quickly after the fact and missed by space.
You’re welcome and for the most part it’s simple.
The most advanced thing is adding an AppleScript record with piped variables (†), e.g., |name|
to the list of possible matches…
if theTask is not "" then copy {|name|:theTask, |note|:"Link to DEVONthink document: ", |URL|:recordID} to end of theTasks
Building the list could have been handled differently but this is a valid form and one some people may not be aware is even possible. It also expedites creating a more complete list to pass to a handler.
(†) Note the pipes are required in this case because name
and URL
mean something to DEVONthink; they’re reserved terms. Bookending the term with the pipes lets it function as a variable. (|note|
isn’t reserved but I used the same syntax to appear uniform.) I could just as easily have used e.g.,…
{theName:theTask, theNote:"Link to DEVONthink document: ", theURL:recordID} to end of theTasks
or
{theTask,"Link to DEVONthink document: ",recordID}`
1 Like
The thing is, that the code is even worse than I thought. It actually does do the right thing in extractTaskText
, namely ignoring any leading spaces (and that should include tab
characters, since it’s using [[:space:]]
). And it does that on lines that have been trimmed before.
Well, it’s only AI. So it has no idea what it is doing.
Although @BLUEFROG beat me to it, here’s my take on the task, in JavaScript. The script is modelled after the original A"I" one. Its idea is to use regular expression’s capturing group in the forEach
loop. It thus saves the part after the checkbox as name
for later use, as well as the current index in the list of paragraphs. Thus, this information is readily available when the OF note is created and the original paragraphs must be updated. The script thus does a bit more than the one by @BLUEFROG 
The code is much shorter than the original version, it doesn’t rely on external command line tools like sed
, it doesn’t repeat itself unnecessarily, and it is (of course
) a lot cleaner, too. IMO. However, it is not tested completely as I do not have OmniFocus to do so. I only checked it against the simple example @hmartin posted, and that was correctly found as well as updated.
(() => {
/* define instances for DT, OF, and current application */
const dtApp = Application('DEVONthink');
const currentApp = Application.currentApplication();
currentApp.includeStandardAdditions = true;
//const ofApp = Application('OmniFocus');
/* Get the selected markdown records, bail out if none selected */
const records = dtApp.selectedRecords().filter(r => r.recordType() === 'markdown');
if (!records || records.length === 0) {
currentApp.displayAlert('No markdown records selected');
return;
}
/* Get first of selected record and its text and URL */
const record = records[0];
const txt = record.plainText();
const link = record.referenceURL();
/* Split the text into paragraphs, stored in an Array */
const lines = txt.split(`\n\n`);
const taskList = [];
/* Find all tasks not added to OF yet and save their index and name to an element of taskList */
lines.forEach((l,i) => {
const match = l.match(/^\s*- \[ \]\s?(.*)/) ;
if (match && !l.includes('(OF)')) {
taskList.push({index: i, name: match[1]});
}
})
/* Bail out if no pending tasks found */
if (taskList.length === 0) {
currentApp.displayAlert('No new tasks found');
return;
}
/* Add each task to OF */
taskList.forEach(t => {
inbox = ofApp.defaultDocument.inboxTasks;
task = of.InboxTask({
name: t.name,
note: `Link to DEVONthink document: ${link}`
});
inbox.push(task);
/* Append ' (OF)' to the text of the task just added. Use the previously saved index to find the correct line */
lines[t.index] += ' (OF)';
})
/* Update the record's plainText */
record.plainText = lines.join('\n\n');
currentApp.displayAlert('Tasks have been created in OmniFocus and marked in the Markdown document.')
})()
1 Like
The Example for Things3 should be possible with “parse tasks into it with transport text” in Omnifocus?
I think there is a middleground @chrillek
I suspect using an advanced model such as Claude 3.7 you could give instructions regarding how to write or refactor the code - such as do not use command-line apps, use a modular design with functions rather than repetition, use the pipe feature to reference variables, etc.
If you then turned those into .rules and updated them after prompting AI for a few more scripts, the result would likely be much improved.
Or alternatively - if your .rules included a few example scripts written with optimal techniques, you could then instruct AI to “Write a JXA script to do _____ in the style of @chrillek .”
It’s no different than prose- AI is pretty good at mimicking writting style from a 5-year-old child to a Southern preacher to a PhD scientist. You just have to tell it what you want.
I can’t say for sure as I don’t use OmniFocus here. However, I’m quite sure it’s possible to use the code and modify the handler for use with OF.
Basically, I’ll tell the AI how I’d write the code. That might teach the AI, true.
But with that level of detail, I could easily write the code myself. And I don’t see myself as teacher for a system that freeloads to learn and then charges others to put what it learned to work.
2 Likes