Create or Open with URL Command

I was thinking of a simple way to connect my OmniFocus with documentation in DEVONthink.
Sometimes for projects in OF, I need a project folder, which I have in a project database as Group.

The idea now is to have a link (button later) in OF, which opens the group if it exists or creates and opens it if not. The name I would pass from the selected item title in OF.

I tried with createGroup, but it always creates the group over and over:

x-DEVONthink://createGroup?destination=F678F6D3-ED1D-48DB-9ACA-1F6CD62CA637&title=Test23

The createMarkdown is not working at all; it just opens Devonthink:
x-DEVONthink://createGroup?destination=F678F6D3-ED1D-48DB-9ACA-1F6CD62CA637&title=Test23

Is there any way to get it only to create it if it doesn’t exist with the same title and open it instead?
I’m hoping for a KISS solution without the need for scripting.

Thank you in advance,
Bastian

I don’t think you can achieve your goal with a URL command. But the scripting dictionary has a command does exactly what you want: create location (createLocation in JavaScript).

I don’t have OmniFocus, so I can’t help with that part… But I think you can keep it pretty short and simple:

(() => {
const DT = Application('DEVONthink');
const OF = Application('OmniFocus');

const dbPath = '~/path/to/database';
const db = DT.openDatabase(dbPath);
if (!db) throw `Database not found: ${dbPath}`;

const pName = /* however you access the project name in OmniFocus */
const pNameEscaped = pName.replaceAll(/\//g, '\\/');
const pGroup = DT.createLocation(`/${pNameEscaped}`, {in: db});
DT.openWindowFor(pGroup, {enforcement: true});

})()

You posted the same URL for both the group and markdown example?

The idea now is to have a link (button later) in OF, which opens the group if it exists or creates and opens it if not. The name I would pass from the selected item title in OF.

An item link is not an application. It is a pointer to something, sometimes with optional parameters. It’s not going to determine the existence of things.

I see; thank you for that suggestion, but Create is not the problem. Would this check if the location does not already exist? And can I trigger this script then via a URL command? Or even better, maybe if OmniFocus supports it (Application() it’s what I’m not sure about), I can build that into an OmniFocus plugin and solve all future problems and ideas of mine as well :slight_smile: Just where can I buy the spare time to play around with that :slight_smile: But as said, thank you :slight_smile:

Oops, never do two things at the same time… Sorry.
I use the same URL; just createMarkdown instead of createGroup. Copied the name from the document. Maybe the parameters are the problem… But no need to investigate: that is anyway not the way to solve my original problem :slight_smile:

Ok, already played around. The OmniFocus plugins have no access to the application object.

That is what I have built so far with the Url Commands:

/*{
  "type": "action",
  "targets": ["omnifocus"]
}*/

(() => {
    const action = new PlugIn.Action(function (selection, sender) {
    const task = selection.tasks.length > 0 ? selection.tasks[0] : null;
    const project = (!task && selection.projects.length > 0) ? selection.projects[0] : null;

    if (!task && !project) {
      new Alert(
        "No Selection",
        "Choose Project"
      ).show();
      return;
    }

    const item = task || project;
    const itemType = task ? "Task" : "Projekt";
    const name = item.name;

    // Escape special characters for URL
    const encodedName = encodeURIComponent(name);
    const dbID = "F678F6D3-ED1D-48DB-9ACA-1F6CD62CA637" 
    
    const devonthinkURL = `x-devonthink://creategroup?title=${encodedName}&destination=${dbID}`;
    
    try {
      URL.fromString(devonthinkURL).open();
    } catch (error) {
      new Alert(
        ` Error: ${error.message}`
      ).show();
    }

  });

  action.validate = function (selection, sender) {
    const hasOneTask = selection.tasks.length === 1 && selection.projects.length === 0;
    const hasOneProject = selection.projects.length === 1 && selection.tasks.length === 0;
    return hasOneTask || hasOneProject;
  };

  return action;
})();

Is there maybe something with URLs that I could do to check if I need to create the Group? So that I can call it from OmniFocus? I will ask in the OmniFocus Forum as well; perhaps someone there has DEVONthink experience.

I have some really jankie old scripts that do some of this. One, trigger from OF, opens a directory on disk that uses the project path as the directory elements, creating paths as needed; another opens a default document in such a directory (I think these were old Paul Korn scripts at one stage - I’ve been successively butchering them ever since). As I index that same directory root into DEVONthink, it works fairly well at mapping between. However, I never sat down to properly map them all together, and the original, unbutchered scripts still refer to older versions of software and AppleScript dictionaries, so I’m averse to just posting them here. But I’m also commenting to keep an eye on this thread, as it’s a noble aim (and some of the new OO features may fit into this project workflow nicely too)

The first parameter of createLocation is a DEVONthink group hierarchy written like a POSIX path. It only creates a location if it doesn’t already exist. If it does, you get an object specifier for the existing one.

(The same goes for openDatabase – it only opens the database if necessary.)

Note that it will create all groups in the path that don’t exist yet.

  • In a new database without any groups,
    createLocation("/Projects/Work/SomeProject", {...})
    would create 3 groups and return an object specifier for the last one.
  • If we say /Projects/Work already exists, only the last missing group would be created.

If you want a separate check first, you could call existsRecordAt or getRecordAt, but I don’t see the point. Your were looking for:

[a solution that] opens the group if it exists or creates and opens it if not

– and that’s exactly what createLocation does. (Well, opening the group is a separate command).


What I posted was a snippet of JXA. I forgot that Omni has their own JavaScript environment… But I would assume it has some way to interface with other applications? Does it have to be a URL?

If you have to work with URLs, one idea to investigate is using Apple Shortcuts. It has its own URL scheme. I think it can return output with x-callback-urls. And shortcuts can execute AppleScript/JXA.

You don’t say whether you aim for something cross-platform. That would get a little more complicated, as DTTG doesn’t support AppleScript/JXA. It has rich support for shortcuts – but DT on the mac doesn’t. So your plugin would need to take that into account and do different things depending on the platform.

I just downloaded OmniFocus to take a quick look at the scripting dictionary. It’s pretty extensive. Is there a particular reason you want to implement this via Omni Automation instead of JXA/AppleScript?

Anyways, there’s a whole section on the Omni Automation website about working with Shortcuts. You should be able to pass data from an Omni action to either a AppleScript/JXA action or a DEVONthink To Go action.

It looks like you can run shortcuts from inside the application after your set them up, see Omni Automation: Calling Shortcuts

2 Likes

Hi @troejgaard,

Thank you for taking that time to even check in on OmniFocus.

It only needs to work on my Mac. My work is not possible on mobile devices, so I barely use either OF or DEVONthink there. Only to look something up when I’m not near my MacBook.

Yes, but this createLocation will create the group over and over? So that would be a second Script I need.

Yes, that is what I intend to do, in fact:

And if DEVONthink had Shortcut support, I would be happy to use that too. But the solution I hope to get would work without extra scripts besides the OF plugin. I’m a programmer with a currently heavy load. I don’t want to end up once more in a rabbit hole where I customize every app and spend more time than it saves in the end. It happened before already when I scripted around Notion, Evernote, Todoist… So I want to keep it simple and stupid. So I was hoping for an existing interface in DEVONthink, which I just can call.

But it seems the compromise would be an Apple Script which does:
Check if the group exists; if not, create it, and then just open the group…

But what I really was hoping for would be something like this:

x-devonthink://upsertGroup

Then I would need nothing more than the short, linked OF plugin.

No. createLocation only creates the group if necessary. Otherwise it returns the existing group. I tried to explain that already, sorry if it was unclear.

Your Github link looks like the exact same plug-in code you posted earlier? I don’t see anything related to Shortcuts in it.

To me the simplest solution still seems to be a JXA script like the one I suggested at the start. I don’t get why you’re so focused on using Omni Automation if you only need this to work on macOS?

When I first heard about Omni Automation I was intrigued, but I didn’t know any JavaScript back then. I only took another look now, so maybe I’m missing something.

I’m more familiar with OmniOutliner, so I’ll use that for an example: creating a group hierarchy in DEVONthink from selected outline rows.

With Omni Automation it does seem like I need to use Shortcuts. I build one consisting of two parts:

And then a plug-in calling the shortcut:
(I just used the first template from the link in my previous post)

/*{
	"type": "action",
	"targets": ["omnioutliner"],
	"author": "Otto Automator",
	"identifier": "com.omni-automation.all.launch-shortcut",
	"version": "1.0",
	"description": "This action will launch the specified Shortcuts workflow.",
	"label": "DEVONthink group hierarchy from selected rows",
	"shortLabel": "Rows → DT Group Hierarchy",
	"paletteLabel": "Shortcut",
	"image": "gearshape.2.fill"
}*/
(() => {
	const action = new PlugIn.Action(function(selection, sender){
		let shortcutTitle = "DT group hierarchy from selected rows";
		shortcutTitle = encodeURIComponent(shortcutTitle);
		let urlStr = "shortcuts://run-shortcut?name=" + shortcutTitle;
		URL.fromString(urlStr).open();
	});
	
	action.validate = function(selection, sender){
		return true;
	};
	
	return action;
})();

Instead I can do the same with a single, short JXA script:

(() => {
	const DT = Application("DEVONthink");
	const OO = Application("OmniOutliner");
	const sel = OO.documents()[0]?.selectedRows;
	if (sel?.length > 0) {
		OO.expelAncestors(sel).forEach(row => {
			const pathStr = row.rowpath.name().map(
				name => name.replaceAll(/\//g, "\\/")
			).join("/");
			DT.createLocation(`/${pathStr}`,
				{in: DT.currentDatabase}
			);
		})
	}
})()

Maybe I could merge the Omni Automation Script from the shortcut into the plug-in code directly to make it shorter, but the pure JXA solution still seems simpler.

2 Likes

Thanks, everybody, for the help. Since I had no good feeling about the solution, which required external scripts, I won’t follow the idea to link DEVONthink from OmniFocus anymore.

The use case is to 95% anyway to add notes about my process; I use Agenda now.
First, my KISS solution with URL callbacks (what I wanted from DEVONthink) works there:

This whole script was meant as proof of concept, written by AI, but it works so well that I keep it.

Furthermore, in Agenda is an integrated MCP server. So after I do real programming work, I can ask the Visual Code AI to summarize what I’ve done with the last commit or in my feature branch (it’s open source anyway), and write that directly as a note to Agenda in the project folder.

Then when the project is finished, I will archive it as a Textbundle into DEVONthink. There, sadly, I still can’t edit Textbundle (different topic here in the forum), but at least search for it and show it.

That is the simple solution I was looking for, without any external scripts and 100% in OmniFocus using the interface of the other “party” directly.

The MCP part was not planned but seems a great addition. I don’t trust AI for real programming (more than simple functions), but summarizing should work well :slight_smile:

So thanks again, all, for the help, and I hope at least my final solution can help others.