Flexible document numbering (ASN) with DevonThink
The Problem
I digitize every important physical document and organize it in the extremely powerful DevonThink. I rarely need the physical copy—retrieving the digital version is faster and easier. To bridge the physical and digital worlds and be able to find the physical document when needed, I assign a unique number to each document and write it on both versions. The great Paperless project calls this ASN (archive serial number).
DevonThink Standard functionality
To assign an ASN, I use DevonThink’s alias and Imprinter feature (Settings → Imprinter) to embed the identifier in the digital file and make it searchable. I assign an alias via a smart rule and trigger the imprinter from there, too.
A document in DevonThink that is imprinted with its alias
DevonThink provides several numbering options:
- Index/Counter: Not useful—they reset on each execution.
- Bates Numbering: Not optimal for my usecase.
The issues I have with Bates Numbering are:
- It cannot be reset or changed between entities or databases. I might want to use Private-ASN 001 and Work-ASN 001, but only am offered one index.
- More importantly, Bates Numbering numbers pages, not documents. As I only need the numbering for document retrieval, I might get gaps between documents, if they have more than 1 page. If I start at Bates Number 1 and have a document of 5 pages, the next document will have the Bates Number 6, and not 2. Did I lose documents 2-5 or did they never exist? I cannot be sure.
The Solution
Continue using the Imprinter and add a Smart Rule that sets aliases via a JavaScript for Automation (JXA) script.
This script
- Can run different counters in different copies of the script (in this example "ASN", but I could now have two smart rules that could each reference "Private-ASN" or "Work-ASN". This can be configured by modifying line
const COUNTER_KEY = "ASN";
- Stores the document counter in a Property List file, the folder can be configured by modifying line
let nsPath = $("~/Databases/Numbering.plist").stringByStandardizingPath;
// Import Foundation framework
ObjC.import('Foundation');
// Configurable key name for the counter in the plist
const COUNTER_KEY = "ASN";
// Resolve full path to the property list file
let nsPath = $("~/Databases/Numbering.plist").stringByStandardizingPath;
/**
* Converts a property list (plist) string into an NSDictionary
*/
function propertyListToDictionary(plist) {
const data = $(plist).dataUsingEncoding($.NSUTF8StringEncoding);
const err = Ref();
const format = Ref();
return $.NSPropertyListSerialization.propertyListWithDataOptionsFormatError(
data,
{},
format,
err
);
}
/**
* Updates aliases in each record with a running counter and saves back the counter
*/
function performsmartrule(records) {
const app = Application.currentApplication();
app.includeStandardAdditions = true;
// Load and parse plist contents
let contents = $.NSString.stringWithContentsOfFileEncodingError(
nsPath,
$.NSUTF8StringEncoding,
null
);
// Read the full dictionary and unwrap it
let fullDict = ObjC.deepUnwrap(propertyListToDictionary(contents));
// Extract and track the current counter, set to 1 if cannot be read.
let counter = fullDict[COUNTER_KEY] || 1;
records.forEach(r => {
let al = r.aliases();
r.aliases = (al.length > 0 ? al + ", " : "") + COUNTER_KEY + " " + counter;
counter++;
});
// Update the counter in the original dictionary
fullDict[COUNTER_KEY] = counter;
// Write the updated dictionary back to the file
let updatedDict = $.NSDictionary.dictionaryWithDictionary(fullDict);
updatedDict.writeToFileAtomically(nsPath, true);
}
Create an imprinter that imprints the alias on the document, and a smart rule that sets the alias, and then imprints. Write the resulting ASN on your physical document and be always able to reference between both copies.
Imprinter under Settings -> Imprinter
Smart rule with JXA execution and imprinter