CLI
  • Curio
  • CLI
  • Forums
  • Support
  • Examples
  • Docs
  • Release Notes
  • Examples
  • Docs
  • Release Notes
  • Download
Curio CLI Examples/Link Curio with DEVONthink

Integration

Link Curio with DEVONthink

Use the Curio CLI to link a file-backed figure to its DEVONthink record, store a link back to Curio, and import files from Finder.

Status
Community
Updated
September 15, 2026
By
Pavan (pdadlani); scripts adapted by Zengobi
Live Curio editing Curio 34.0+
Download Curio and DEVONthink scripts v1.0.0 34 KB View complete source

Gallery

Pavan's original Keyboard Maestro workflow. The downloadable adaptation passes the DEVONthink link through the Curio figure's URL instead of a Keyboard Maestro variable.

Link a Curio figure with its DEVONthink record

Pavan (pdadlani) shared his workflow on the Zengobi forums. He uses the Curio CLI to connect figures in his projects with files indexed in DEVONthink. A figure's URL opens the DEVONthink record, and a custom metadata field on that record links back to the figure in Curio.

His original workflow uses Keyboard Maestro to trigger two scripts and report errors. The gallery image shows that arrangement. The downloadable adaptation keeps the two steps but stores the handoff in the figure's URL, so Keyboard Maestro is optional.

The CLI calls

The first step reads the selected figure's file path and identity:

set jsonText to do shell script "/usr/local/bin/curio get --selected figures --fields id,asset_file,link,url"

The script requires exactly one selection, then asks DEVONthink to find records indexing that exact path across its open databases. With one unique match, it writes the record's x-devonthink-item:// URL onto the captured figure:

do shell script "/usr/local/bin/curio set --id " & quoted form of figureID & " --url " & quoted form of devonLink

The second step reads the figure's link and url together. It checks that the DEVONthink URL still identifies the record indexing the figure's file, then writes the curio:// link into that record's curiolink custom metadata field. DEVONthink accepts an item URL directly in its get record with uuid command.

/usr/local/bin/curio is the symlink created by the CLI installer. Using that absolute path avoids configuring the shell's PATH in every call. CLI responses are JSON; the scripts use macOS's built-in Foundation JSON parser to decode strings and check responses. No jq installation or manual slash replacement is needed.

Try the scripts

Install the CLI using Curio > Install Curio CLI. Open a saved Curio project and the DEVONthink database that indexes the file. The download includes readable .applescript sources and compiled .scpt files.

  1. Select exactly one file-backed figure in Curio.
  2. Run Curio - Get dthinkUUID and set it to figureURL.scpt. It returns the DEVONthink item URL after updating the figure.
  3. With that same figure selected, run Curio - Get curiolink - Set dthink custom-metafield.scpt. It returns the Curio figure link after writing and reading back the metadata.

Keep the project active while the scripts run. They can be opened in Script Editor or invoked by an automation launcher; allow the macOS automation permissions requested by the launcher. The first script captures the figure UUID before contacting DEVONthink, so changing the selection during lookup does not redirect its write to a different figure.

The scripts replace the figure's existing URL and the record's existing curiolink value. A record therefore holds one Curio backlink; linking it from another figure replaces that backlink. These are two separate operations: if the second step fails, the figure's DEVONthink URL remains and you can retry the second step.

Matching and error handling

The adaptation deliberately requires the same file path in both applications. An alias to a file indexed in DEVONthink suits this workflow. A separately copied or moved file may have a different path and will need to be indexed at that location before it can match.

The scripts stop on no selection, multiple selections, missing fields, malformed JSON, unavailable databases, ambiguous records, and failed writes. They do not guess from a matching filename. Replicants sharing the same DEVONthink UUID count as one record.

In Keyboard Maestro, the first script's successful result can still be saved to dthink_item_link, but the revised second script does not need that variable. Configure the macro to stop and show an error when a script fails. DEVONTHINK_NOT_FOUND is now part of an AppleScript error message, rather than a successful return value; a macro that only compares the returned text must be updated. No Keyboard Maestro macro is included.

Import a file from Finder

Three additional scripts demonstrate the CLI's file transfer modes:

/usr/local/bin/curio import '/path/to/file.pdf' --transfer copy
/usr/local/bin/curio import '/path/to/file.pdf' --transfer alias
/usr/local/bin/curio import '/path/to/file.pdf' --transfer move

Select one file in Finder and run the corresponding Copy, Alias, or Move script to import it into Curio's current idea space. Copy stores a copy in the project. Alias references the existing file. Move transfers the file into the project, removing it from its original location.

For Shortcuts, the download includes Shortcut - Import File.applescript. Create a shortcut with Get Selected Files in Finder, followed by Run AppleScript, and paste that source into the action. Set transferMode to copy or alias. It accepts exactly one file; do not add a repeat loop around it. The original signed Shortcuts are not part of the revised download.

About this contribution

Pavan supplied the original scripts and workflow diagram. Read his original forum post and join the discussion. Zengobi adapted the scripts for readable source, explicit error handling, and use without Keyboard Maestro. Syntax and JSON handling have been checked; live Curio–DEVONthink linking and the Shortcuts action still need end-to-end testing. Pavan also describes using the links from exported PDFs on an iPad; that part of his setup has not been verified here.

Inspect before downloading

Complete source

7 files

Every text file included in the download is available here. Open a filename to inspect its contents.

README.md3 KB
# Curio and DEVONthink

Adapted by Zengobi from [Pavan (pdadlani)'s contributed workflow](https://forums.zengobi.com/t/curio-devonthink-automated-intercommunication-workflow/5020).

## Requirements

Install the Curio CLI from Curio's menu. These scripts call `/usr/local/bin/curio` directly. Open a saved project in Curio 34 or later. For linking, open the DEVONthink databases containing the indexed file. Allow automation permissions for the app running the scripts. Keyboard Maestro and jq are not required.

## Link in both directions

Select exactly one file-backed figure in Curio and run these compiled scripts in order (or open their matching sources in Script Editor):

1. **Curio - Get dthinkUUID and set it to figureURL.scpt** finds one unique DEVONthink record indexing the exact asset path and replaces the figure's URL with that record's item URL. It returns the item URL.
2. **Curio - Get curiolink - Set dthink custom-metafield.scpt** reads the same figure's stored URL and native Curio link, verifies the record against the asset path, and replaces its `curiolink` metadata. It returns the Curio link after checking the saved metadata.

Keep the same figure selected between steps and keep the project active during execution. Each record has one `curiolink` value: linking another figure overwrites it. Step 2 can be retried after a failure; step 1's URL write is not rolled back.

An alias to an indexed file is a good starting point. Copies and moved files can have different paths and must be indexed at their actual locations. No filename fallback is used. Missing matches raise an error containing `DEVONTHINK_NOT_FOUND`; ambiguous matches and other failures also raise errors. Replicants with the same UUID count once.

Keyboard Maestro can run the scripts and save step 1's result to `dthink_item_link`, but step 2 reads the figure's URL instead of that variable. Configure your macro to stop and display script errors; do not rely only on checking a successful result for the old sentinel string. No macro is supplied.

## Finder imports

The Copy, Alias, and Move scripts require exactly one selected Finder file and import into Curio's current idea space. Copy stores a copy, Alias references the existing file, and Move removes the source from its original location as it transfers it into the project. Folders and multiple selections are rejected. CLI errors propagate to the caller.

## Shortcuts

Create a shortcut containing **Get Selected Files in Finder**, then **Run AppleScript**. Paste `Shortcut - Import File.applescript` into the latter action. Leave `transferMode` as `copy` for Import File or change it to `alias` for Alias File. Pass the Finder action's output directly; no repeat action is needed. This action requires exactly one file.

The original signed `.shortcut` files were not revised and are excluded from this download. The supplied action source is their replacement, not a signed installable shortcut.

## Validation

The scripts were compiled against the installed app dictionaries. The JSON parser and selection validation were tested with synthetic responses, including escaped strings and invalid data. No live projects, records, or source files were modified during validation. Curio–DEVONthink linking and Shortcuts execution still require end-to-end testing. PDF and iPad link behavior is not verified.

Readable `.applescript` files are the source of truth. To rebuild a compiled script on a Mac with DEVONthink installed:

```bash
osacompile -o 'Example.scpt' 'Example.applescript'
```
Curio - Alias File into Curio.applescript569 bytes
-- Adapted by Zengobi from Pavan (pdadlani)'s Curio CLI example.
-- Imports exactly one Finder selection into Curio's current idea space.
tell application "Finder"
	set selectedFiles to selection
	if (count of selectedFiles) is not 1 then error "Select exactly one file in Finder."
	set selectedFile to item 1 of selectedFiles as alias
end tell
if folder of (info for selectedFile) then error "Select a file, not a folder."
set filePath to POSIX path of selectedFile
return do shell script "/usr/local/bin/curio import " & quoted form of filePath & " --transfer alias"
Curio - Copy File into Curio.applescript568 bytes
-- Adapted by Zengobi from Pavan (pdadlani)'s Curio CLI example.
-- Imports exactly one Finder selection into Curio's current idea space.
tell application "Finder"
	set selectedFiles to selection
	if (count of selectedFiles) is not 1 then error "Select exactly one file in Finder."
	set selectedFile to item 1 of selectedFiles as alias
end tell
if folder of (info for selectedFile) then error "Select a file, not a folder."
set filePath to POSIX path of selectedFile
return do shell script "/usr/local/bin/curio import " & quoted form of filePath & " --transfer copy"
Curio - Get curiolink - Set dthink custom-metafield.applescript5 KB
-- Adapted by Zengobi from Pavan (pdadlani)'s Curio CLI examples.
use framework "Foundation"
use scripting additions

-- Run after step 1 with the same figure selected. Keyboard Maestro is optional.
set figureInfo to my selectedFigure()
set curioLink to my requiredText(figureInfo, "link")
set devonLink to my requiredText(figureInfo, "url")
set targetPath to my requiredText(figureInfo, "asset_file")
if curioLink does not start with "curio://" then error "Curio did not return a figure link."
if devonLink does not start with "x-devonthink-item://" then error "Run step 1 first: this figure needs a DEVONthink item URL."

-- Verify that the stored URL still belongs to the record indexing this file.
set pathRecord to my matchingRecord(targetPath)
tell application id "DNtp"
	set theRecord to get record with uuid devonLink
	if theRecord is missing value then error "The linked DEVONthink record is not available."
	if (uuid of theRecord as text) is not (uuid of pathRecord as text) then error "The figure's DEVONthink URL does not match its file. Run step 1 again."
	set didWrite to add custom meta data curioLink for "curiolink" to theRecord
	if not didWrite then error "DEVONthink could not write the curiolink metadata."
	set savedLink to get custom meta data for "curiolink" from theRecord
	if savedLink is missing value then error "DEVONthink did not retain the curiolink metadata."
	if (savedLink as text) is not curioLink then error "DEVONthink returned a different curiolink value after writing."
end tell
return curioLink

on cliResult(arguments)
	set jsonText to do shell script "/usr/local/bin/curio " & arguments
	return my parseResponse(jsonText)
end cliResult

on parseResponse(jsonText)
	set jsonData to (current application's NSString's stringWithString:jsonText)'s dataUsingEncoding:(current application's NSUTF8StringEncoding)
	set {response, parseError} to current application's NSJSONSerialization's JSONObjectWithData:jsonData options:0 |error|:(reference)
	if response is missing value then error "Curio returned invalid JSON: " & (parseError's localizedDescription() as text)
	if not (response's isKindOfClass:(current application's NSDictionary)) then error "Curio returned an unexpected response."
	set okValue to response's objectForKey:"response_ok"
	if okValue is missing value then error "Curio response is missing response_ok."
	if not (okValue's isEqual:(current application's NSNumber's numberWithBool:true)) then error "Curio reported a failure: " & jsonText
	set resultValue to response's objectForKey:"result"
	if resultValue is missing value then error "Curio response is missing result."
	if not (resultValue's isKindOfClass:(current application's NSDictionary)) then error "Curio returned an unexpected result."
	return resultValue
end parseResponse

on requiredText(objectValue, fieldName)
	set fieldValue to objectValue's objectForKey:fieldName
	if fieldValue is missing value then error "Curio did not return " & fieldName & "."
	if not (fieldValue's isKindOfClass:(current application's NSString)) then error "Curio did not return text for " & fieldName & "."
	if (fieldValue's |length|() as integer) is 0 then error "Curio returned an empty " & fieldName & "."
	return fieldValue as text
end requiredText

on selectedFigure()
	set resultValue to my cliResult("get --selected figures --fields id,asset_file,link,url")
	set figures to resultValue's objectForKey:"items"
	if figures is missing value then error "Select exactly one file-backed figure in Curio."
	if not (figures's isKindOfClass:(current application's NSArray)) then error "Curio returned an unexpected selection."
	if (figures's |count|() as integer) is not 1 then error "Select exactly one file-backed figure in Curio."
	return figures's objectAtIndex:0
end selectedFigure

on matchingRecord(targetPath)
	set recordIDs to {}
	set matchedRecord to missing value
	tell application id "DNtp"
		set openDatabases to databases
		if (count of openDatabases) is 0 then error "Open the DEVONthink database that indexes this file."
		repeat with thisDatabase in openDatabases
			set matches to lookup records with path targetPath in thisDatabase
			if matches is not missing value then
				repeat with candidate in matches
					set candidateID to uuid of candidate as text
					if recordIDs does not contain candidateID then
						set end of recordIDs to candidateID
						set matchedRecord to contents of candidate
					end if
				end repeat
			end if
		end repeat
	end tell
	if (count of recordIDs) is 0 then error "DEVONTHINK_NOT_FOUND: No record indexes this exact file path."
	if (count of recordIDs) is not 1 then error "More than one DEVONthink record matches this path. Resolve the duplicate records before linking."
	return matchedRecord
end matchingRecord
Curio - Get dthinkUUID and set it to figureURL.applescript4 KB
-- Adapted by Zengobi from Pavan (pdadlani)'s Curio CLI examples.
use framework "Foundation"
use scripting additions

-- Select one file-backed figure in Curio, then run this script.
set figureInfo to my selectedFigure()
set figureID to my requiredText(figureInfo, "id")
set targetPath to my requiredText(figureInfo, "asset_file")
set figureLink to my requiredText(figureInfo, "link")
set theRecord to my matchingRecord(targetPath)
tell application id "DNtp" to set devonLink to reference URL of theRecord as text
if devonLink does not start with "x-devonthink-item://" then error "DEVONthink did not return an item link."

-- Address the captured figure by UUID, not whatever is selected after lookup.
set currentFigure to my cliResult("get --id " & quoted form of figureID & " --fields link,asset_file")
if my requiredText(currentFigure, "link") is not figureLink then error "The Curio project context changed. Run the script again."
if my requiredText(currentFigure, "asset_file") is not targetPath then error "The figure's file changed. Run the script again."
set writeResult to my cliResult("set --id " & quoted form of figureID & " --url " & quoted form of devonLink)
return devonLink

on cliResult(arguments)
	set jsonText to do shell script "/usr/local/bin/curio " & arguments
	return my parseResponse(jsonText)
end cliResult

on parseResponse(jsonText)
	set jsonData to (current application's NSString's stringWithString:jsonText)'s dataUsingEncoding:(current application's NSUTF8StringEncoding)
	set {response, parseError} to current application's NSJSONSerialization's JSONObjectWithData:jsonData options:0 |error|:(reference)
	if response is missing value then error "Curio returned invalid JSON: " & (parseError's localizedDescription() as text)
	if not (response's isKindOfClass:(current application's NSDictionary)) then error "Curio returned an unexpected response."
	set okValue to response's objectForKey:"response_ok"
	if okValue is missing value then error "Curio response is missing response_ok."
	if not (okValue's isEqual:(current application's NSNumber's numberWithBool:true)) then error "Curio reported a failure: " & jsonText
	set resultValue to response's objectForKey:"result"
	if resultValue is missing value then error "Curio response is missing result."
	if not (resultValue's isKindOfClass:(current application's NSDictionary)) then error "Curio returned an unexpected result."
	return resultValue
end parseResponse

on requiredText(objectValue, fieldName)
	set fieldValue to objectValue's objectForKey:fieldName
	if fieldValue is missing value then error "Curio did not return " & fieldName & "."
	if not (fieldValue's isKindOfClass:(current application's NSString)) then error "Curio did not return text for " & fieldName & "."
	if (fieldValue's |length|() as integer) is 0 then error "Curio returned an empty " & fieldName & "."
	return fieldValue as text
end requiredText

on selectedFigure()
	set resultValue to my cliResult("get --selected figures --fields id,asset_file,link,url")
	set figures to resultValue's objectForKey:"items"
	if figures is missing value then error "Select exactly one file-backed figure in Curio."
	if not (figures's isKindOfClass:(current application's NSArray)) then error "Curio returned an unexpected selection."
	if (figures's |count|() as integer) is not 1 then error "Select exactly one file-backed figure in Curio."
	return figures's objectAtIndex:0
end selectedFigure

on matchingRecord(targetPath)
	set recordIDs to {}
	set matchedRecord to missing value
	tell application id "DNtp"
		set openDatabases to databases
		if (count of openDatabases) is 0 then error "Open the DEVONthink database that indexes this file."
		repeat with thisDatabase in openDatabases
			set matches to lookup records with path targetPath in thisDatabase
			if matches is not missing value then
				repeat with candidate in matches
					set candidateID to uuid of candidate as text
					if recordIDs does not contain candidateID then
						set end of recordIDs to candidateID
						set matchedRecord to contents of candidate
					end if
				end repeat
			end if
		end repeat
	end tell
	if (count of recordIDs) is 0 then error "DEVONTHINK_NOT_FOUND: No record indexes this exact file path."
	if (count of recordIDs) is not 1 then error "More than one DEVONthink record matches this path. Resolve the duplicate records before linking."
	return matchedRecord
end matchingRecord
Curio - Move File into Curio.applescript568 bytes
-- Adapted by Zengobi from Pavan (pdadlani)'s Curio CLI example.
-- Imports exactly one Finder selection into Curio's current idea space.
tell application "Finder"
	set selectedFiles to selection
	if (count of selectedFiles) is not 1 then error "Select exactly one file in Finder."
	set selectedFile to item 1 of selectedFiles as alias
end tell
if folder of (info for selectedFile) then error "Select a file, not a folder."
set filePath to POSIX path of selectedFile
return do shell script "/usr/local/bin/curio import " & quoted form of filePath & " --transfer move"
Shortcut - Import File.applescript696 bytes
-- Paste into Shortcuts' Run AppleScript action, after Get Selected Files in Finder.
-- Set transferMode to "alias" for Alias File, or "copy" for Import File.
property transferMode : "copy"

on run {input, parameters}
	if transferMode is not in {"copy", "alias"} then error "Choose copy or alias as the transfer mode."
	if (count of input) is not 1 then error "Pass exactly one Finder file to this action."
	set selectedFile to item 1 of input as alias
	if folder of (info for selectedFile) then error "Pass a file, not a folder."
	set filePath to POSIX path of selectedFile
	return do shell script "/usr/local/bin/curio import " & quoted form of filePath & " --transfer " & transferMode
end run
SHA-256b172f39e3c2ac63029ba98e315bbe9688f1957c18fe735eaf7a24507720b4a97

  • Copyright © Zengobi, Inc.
  • Contact Us
  • Terms of Use
  • Privacy Policy