Automating RSS Refresh

With the addition of an MCP I’m able to automate capturing a list of what I’ve added to DEVONthink the prior day and output it to a markdown note with links to the original source and the PDF in DEVONthink. I also have list of bookmarks DT pulls in as an RSS feed. I have the RSS feed pulling once a day, but there isn’t away to set what time. I’ve been trying to set a trigger to force a feed refresh a few minutes before I run the DT capture automation in the morning. I’ve seen notes around the web around AppleScript, but nothing detailed how to build a trigger, so I can run a cronjob to kick it off.

Wouldn’t a more frequent interval, e.g. every 15 minutes, fix this issue? Here’s also an AppleScript example for a nice morning brief that doesn’t require MCP. Somewhat bloated as it’s automatically generated but functional :wink:

-- Daily Changes Digest
-- Generated by ...
-- Date: May 31, 2026 at 8:59 AM

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

-- Standalone execution
on run
	tell application id "DNtp"
		set theSearchResults to search "kind:doc modified:#1"
		if (count of theSearchResults) > 50 then set theSearchResults to items 1 thru 50 of theSearchResults
		if (count of theSearchResults) is 0 then return
	end tell
	my processRecords(theSearchResults)
end run

-- Universal entry point for triggered execution
on performWorkflow(theContext)
	my processRecords(|records| of theContext)
end performWorkflow

-- Smart Rule entry point
on performSmartRule(theRecords)
	my processRecords(theRecords)
end performSmartRule

-- Main processing handler
on processRecords(theRecords)
	tell application id "DNtp"
		-- AI Curate: Select most relevant items
		set theCurateSelected to {}
		set theCurateIdx to 0
		repeat with theRec in theRecords
			set end of theCurateSelected to theCurateIdx
			set theCurateIdx to theCurateIdx + 1
		end repeat
		set theCurateCount to 10
		if (count of theRecords) > theCurateCount then
			set theTitles to {}
			repeat with theRec in theRecords
				set end of theTitles to name of theRec
			end repeat
			
			set thePrompt to "You are selecting items for: daily changes briefing" & linefeed & "" & linefeed & "From the following numbered list of items, select the 10 most relevant and valuable items based on these criteria:" & linefeed & "Select the most significant and impactful changes. Prioritize diverse documents and avoid redundant or minor edits." & linefeed & "" & linefeed & "Items:" & linefeed & ""
			set theIdx to 0
			repeat with theTitle in theTitles
				set thePrompt to thePrompt & theIdx & ". <title>" & theTitle & "</title>" & return
				set theIdx to theIdx + 1
			end repeat
			set thePrompt to thePrompt & "" & linefeed & "Respond with ONLY a JSON array of the selected item numbers (0-based indices), no code fences or explanation." & linefeed & "Example: [0, 3, 7, 12, 15]"
			
			try
				set theSelectedIndices to get chat response for message thePrompt as "JSON"
				if theSelectedIndices is not missing value and (count of theSelectedIndices) > 0 then
					set theFilteredRecords to {}
					repeat with theSelIdx in theSelectedIndices
						set theRecIdx to (theSelIdx as integer) + 1 -- AppleScript is 1-based
						if theRecIdx ≥ 1 and theRecIdx ≤ (count of theRecords) then
							set end of theFilteredRecords to item theRecIdx of theRecords
						end if
					end repeat
					if (count of theFilteredRecords) > 0 then
						set theRecords to theFilteredRecords
						set theCurateSelected to theSelectedIndices
					end if
				end if
			end try
		end if
		set ctx_selected to theCurateSelected
		
		set ctx_briefing to ""
		set ctx_record to ""
		set ctx_report to ""
		set ctx_summary to ""
		set theEntriesList to {}
		
		show progress indicator "Daily Changes Digest" steps (count of theRecords)
		try
			repeat with theRecord in theRecords
				step progress indicator (name of theRecord as string)
				set ctx_briefing to ""
				set ctx_record to ""
				set ctx_report to ""
				set ctx_summary to ""
				-- Trigger: Scheduled
				set ctx_recordCount to (count of theRecords)
				set ctx_records to theRecords
				-- AI Curate: Records filtered by pre-loop curation
				-- AI Summarization
				set ctx_summary to ""
				set theNow to current date
				set theWeekday to weekday of theNow as string
				set theYear to year of theNow as string
				set theMonth to (characters -2 thru -1 of ("0" & ((month of theNow) as integer))) as string
				set theDay to (characters -2 thru -1 of ("0" & (day of theNow))) as string
				set theHours to (characters -2 thru -1 of ("0" & (hours of theNow))) as string
				set theMinutes to (characters -2 thru -1 of ("0" & (minutes of theNow))) as string
				set theDateCtx to "Current date: " & theWeekday & ", " & theYear & "-" & theMonth & "-" & theDay & ", " & theHours & ":" & theMinutes & "." & return & return
				set theAIPrompt to theDateCtx & "Summarize the following content in 2-3 sentences, focusing on: changes and additions. Reply with only the summary, no preamble or labels."
				set theAIPrompt to theAIPrompt & return & return & "Respond in plain text only. Do not use any Markdown formatting such as headers, bold, italic, lists, or code blocks."
				try
					set theAIResult to get chat response for message theAIPrompt record theRecord
					if theAIResult is not missing value then set ctx_summary to theAIResult
				end try
				
				set end of theEntriesList to "### [" & (name of theRecord) & "](" & (reference URL of theRecord) & ")" & linefeed & "" & linefeed & "" & ctx_summary
			end repeat
		on error theError
			hide progress indicator
			error theError
		end try
		
		-- Merge accumulated results
		set ctx_report to "# Daily Changes — " & (my isoDate(current date)) & "" & linefeed & "" & linefeed & "" & my joinList(theEntriesList, linefeed & linefeed) & "" & linefeed & ""
		
		-- AI Generation
		set ctx_briefing to ""
		set theGenPrompt to "Rewrite the following structured digest into a friendly, concise daily briefing in Markdown. Keep the document links intact and preserve all key information, but make it read naturally as a short newsletter." & linefeed & "" & linefeed & "" & "<report>" & ctx_report & "</report>"
		try
			set theGenResult to get chat response for message theGenPrompt
			if theGenResult is not missing value then set ctx_briefing to theGenResult
		end try
		
		-- Create new document
		set theDocName to "Daily Changes — " & (my isoDate(current date))
		set theDocContent to ((ctx_briefing) as string)
		set theTargetDB to current database
		set theTargetGroup to create location "/Digests/Daily Changes" in theTargetDB
		set theRecord to create record with {name:theDocName, type:markdown, content:theDocContent} in theTargetGroup
		set ctx_uuid to uuid of theRecord
		if theRecord is not missing value then set ctx_record to {|uuid|:(uuid of theRecord), |name|:(name of theRecord), |url|:(URL of theRecord), |referenceURL|:(reference URL of theRecord), |path|:(path of theRecord), |location|:(location of theRecord), |comment|:(comment of theRecord)}
		
		hide progress indicator
	end tell
end processRecords


-- Join a list into a delimited string
on joinList(theList, theDelimiter)
	set oldDelims to AppleScript's text item delimiters
	set AppleScript's text item delimiters to theDelimiter
	set theResult to theList as string
	set AppleScript's text item delimiters to oldDelims
	return theResult
end joinList

-- Format date as ISO 8601 (yyyy-MM-dd)
on isoDate(theDate)
	set y to (year of theDate) as string
	set m to (month of theDate as integer) as string
	if length of m < 2 then set m to "0" & m
	set d to (day of theDate) as string
	if length of d < 2 then set d to "0" & d
	return y & "-" & m & "-" & d
end isoDate

-- Serialize a value to text (objects → JSON, scalar lists → comma-joined)
on toString(theValue)
	set theIsList to (class of theValue is list)
	if theIsList and (count of theValue) > 0 and (class of (item 1 of theValue) is not in {record, list}) then
		set oldDelims to AppleScript's text item delimiters
		set AppleScript's text item delimiters to ", "
		set theJoined to theValue as string
		set AppleScript's text item delimiters to oldDelims
		return theJoined
	end if
	if theIsList or (class of theValue is record) then
		set theData to (current application's NSJSONSerialization's dataWithJSONObject:theValue options:(current application's NSJSONWritingPrettyPrinted) |error|:(missing value))
		if theData is not missing value then return ((current application's NSString's alloc()'s initWithData:theData encoding:(current application's NSUTF8StringEncoding)) as string)
	end if
	if theValue is missing value then return ""
	return theValue as string
end toString

And for @chrillek also a JavaScript version :smiley:

// Daily Changes Digest
// Generated by ...
// Date: May 31, 2026 at 9:06 AM

const DT = Application("DEVONthink");
DT.includeStandardAdditions = true;
const sa = Application.currentApplication();
sa.includeStandardAdditions = true;

// Standalone execution
(() => {
    const theAllResults = DT.search("kind:doc modified:#1");
    const searchResults = theAllResults.slice(0, 50);
    if (!searchResults || searchResults.length === 0) return;
    processRecords(searchResults || []);
})();

// Universal entry point for triggered execution
function performWorkflow(theContext) {
    processRecords(theContext.records || []);
}

// Smart Rule entry point
function performSmartRule(records) {
    processRecords(records);
}

// Main processing function
function processRecords(records) {
    // AI Curate: Select most relevant items
    var ctx_selected = [];
    for (let __i = 0; __i < records.length; __i++) ctx_selected.push(__i);
    const curateCount = 10;
    if (records.length > curateCount) {
        const titles = records.map(r => r.name());
        let curatePrompt = "You are selecting items for: daily changes briefing\n\nFrom the following numbered list of items, select the 10 most relevant and valuable items based on these criteria:\nSelect the most significant and impactful changes. Prioritize diverse documents and avoid redundant or minor edits.\n\nItems:\n";
        titles.forEach((title, idx) => {
            curatePrompt += `${idx}. <title>${title}</title>\n`;
        });
        curatePrompt += "\nRespond with ONLY a JSON array of the selected item numbers (0-based indices), no code fences or explanation.\nExample: [0, 3, 7, 12, 15]";

        try {
            let selectedIndices = DT.getChatResponseForMessage(curatePrompt, { format: "JSON" });
            if (typeof selectedIndices === 'string') {
                selectedIndices = JSON.parse(selectedIndices);
            }
            if (selectedIndices && selectedIndices.length > 0) {
                const filteredRecords = [];
                for (let i = 0; i < selectedIndices.length; i++) {
                    const idx = selectedIndices[i];
                    if (idx >= 0 && idx < records.length) {
                        filteredRecords.push(records[idx]);
                    }
                }
                if (filteredRecords.length > 0) {
                    records = filteredRecords;
                    ctx_selected = selectedIndices;
                }
            }
        } catch(e) {
            console.log('AI Curate error: ' + e);
        }
    }

    var entriesList = [];

    DT.showProgressIndicator("Daily Changes Digest", { steps: records.length });
    try {
        records.forEach(record => {
            DT.stepProgressIndicator(record.name());
            const ctx = {briefing: "", record: "", report: "", summary: ""};

            {
            // Trigger: Scheduled
            ctx.recordCount = records.length;
            ctx.records = records;
            }
            {
            // AI Curate: Records filtered by pre-loop curation
            }
            {
            // AI Summarization
            ctx.summary = "";
            var theNow = new Date();
            var theDays = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
            var theDateCtx = "Current date: " + theDays[theNow.getDay()] + ", " + theNow.getFullYear() + "-" + String(theNow.getMonth()+1).padStart(2,"0") + "-" + String(theNow.getDate()).padStart(2,"0") + ", " + String(theNow.getHours()).padStart(2,"0") + ":" + String(theNow.getMinutes()).padStart(2,"0") + ".\n\n";
            var theAIPrompt = theDateCtx + "Summarize the following content in 2-3 sentences, focusing on: changes and additions. Reply with only the summary, no preamble or labels.";
            theAIPrompt += "\n\nRespond in plain text only. Do not use any Markdown formatting such as headers, bold, italic, lists, or code blocks.";
            try {
                const theAIResult = DT.getChatResponseForMessage(theAIPrompt, { record: record });
                if (theAIResult) ctx.summary = theAIResult;
            } catch(e) {}

            }
            entriesList.push(`### [${record.name()}](${record.referenceURL()})\n\n${ctx.summary ?? ''}`);
        });
    } catch (e) {
        DT.hideProgressIndicator();
        throw e;
    }

    const ctx = {briefing: "", record: "", report: "", summary: ""};
    var record = (records && records.length > 0) ? records[records.length - 1] : null;
    // Merge accumulated results
    ctx.report = "# Daily Changes — " + (() => { const d = new Date(); return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0'); })() + "\n\n" + entriesList.join("\n\n") + "\n";

    {
    // AI Generation
    ctx.briefing = "";
    var theGenPrompt = `Rewrite the following structured digest into a friendly, concise daily briefing in Markdown. Keep the document links intact and preserve all key information, but make it read naturally as a short newsletter.\n\n<report>${ctx.report ?? ''}</report>`;
    try {
        const theGenResult = DT.getChatResponseForMessage(theGenPrompt);
        if (theGenResult) ctx.briefing = theGenResult;
    } catch(e) {}

    }
    {
    // Create new document
    const theDocName = `Daily Changes — ${(() => { const d = new Date(); return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0'); })()}`;
    const theDocContent = String((ctx.briefing ?? '') ?? "");
    var theTargetDB = DT.currentDatabase();
    var theTargetGroup = DT.createLocation("/Digests/Daily Changes", { in: theTargetDB });
    record = DT.createRecordWith({name: theDocName, type: "markdown", content: theDocContent}, {in: theTargetGroup});
    ctx.uuid = record.uuid();
    if (record) ctx.record = {uuid: record.uuid(), name: record.name(), url: record.url() || "", referenceURL: record.referenceURL(), path: record.path() || "", location: record.location() || "", comment: record.comment() || ""};

    }
    DT.hideProgressIndicator();
}


function toString(v) {
    if (v === null || v === undefined) return "";
    if (Array.isArray(v)) {
        if (v.length > 0 && typeof v[0] === "object" && v[0] !== null) {
            try { return JSON.stringify(v, null, 2); } catch(e) {}
        }
        return v.join(", ");
    }
    if (typeof v === "object") try { return JSON.stringify(v, null, 2); } catch(e) {}
    return String(v);
}

I had an AppleScript version in past years, but it was pulling too much (I have DT monitoring local directories), but just want manual additions I’ve made and the RSS feed (hourly is too frequent, but I may move back to that). I have some monitor in DT that runs frequently and locks up DT when it runs, I only need the DT automations to run once a day as I use it more for reference and rinding related materials.

I may try the JS route, but modify it to Python (my preferred scripting for automations).

Refreshing feeds with AppleScript/JXA is easy enough.

AppleScript:

tell application id "DNtp"
	set allFeeds to search "kind:feed"
	repeat with theFeed in allFeeds
		refresh record theFeed
	end repeat
end tell

JavaScript:

(() => {
	const DT = Application("DEVONthink");
	const feeds = DT.search("kind:feed");
	feeds.forEach(f => DT.refresh({record: f}));
})()

I don’t know much about cronjobs, but I guess you can just save it as a shell script with the proper shebang and permissions?

A more fleshed out JavaScript example:

#!/usr/bin/env osascript -l JavaScript

(() => {
	const DT = Application("DEVONthink");
	if (!DT.running()) DT.launch();
	// Ensure relevant databases are open:
	const dbPaths = [
		"~/path/to/Database_1",
		"~/path/to/Database_2"
	];
	dbPaths.forEach(path => DT.openDatabase(path));

	const feeds = DT.search("kind:feed");
	feeds.forEach(f => DT.refresh({record: f}));
})()

You could also use Automator to create a calendar alarm applet. Basically a little AppleScript application that you can trigger with a calendar alarm. Then you set up a recurring event to trigger it at the desired time.

2 Likes

You don’t need to use Python for this. And running a scheduled automation in DEVONthink is an easy thing. In fact, it’s simple to schedule it to the minute, day, day of the week, etc.

Set up a reminder for a specific feed, with the date as granularly as you want, like 4:14 each day, and an alarm running a script. I opted for an external script so it could be used easily with other feeds.…

This code is a proof of concept but is completely functional and can be modified to suit specific needs…

on performReminder(theRecord)
	tell application id "DNtp"
		if record type of theRecord is not feed then return
		set od to AppleScript's text item delimiters
		refresh record theRecord
		delay 3 -- Slow things down as this is asynchronous and it doesn't have to be delivered quickly.
		
		set newArticles to search "kind:news added:today" in theRecord
		if newArticles is {} then return
		
		set theList to {}
		set feedName to (name of theRecord)
		set shortDate to my shortenDate(current date, od)
		set documentHeader to "# " & feedName & " Articles for " & shortDate & "
		---
Date|Article
		:---|:---
		"
		repeat with theArticle in newArticles
			set {name without extension:recordName, reference URL:recordUUID, creation date:createdDate} to theArticle
			copy (my shortenDate(createdDate, od) & "|[" & recordName & "](" & recordUUID & ")" & linefeed as string) to end of theList
		end repeat
		if theList is not {} then create record with {name:(feedName & " - Articles for " & shortDate), record type:markdown, content:documentHeader & theList as string} in root of (database of theRecord)
	end tell
end performReminder

on shortenDate(theDate, od) -- Optionally switching to hyphen-delimiter date
	set AppleScript's text item delimiters to {"/", "."}
	set tempDate to text items of (short date string of theDate)
	set AppleScript's text item delimiters to "-"
	set tempDate to tempDate as string
	set AppleScript's text item delimiters to od
	return tempDate
end shortenDate

Note: I added a delay in case the feed format wasn’t set to Automatic in the RSS > Feed Format settings or the Generic Info inspector/popover. And I don’t have active enough feeds to test other formats at the moment. However, I don’t believe it’s synchronous so it wouldn’t wait for the refresh before moving on to the next steps.

And the example yielded from one reminder alarm…

And yes, it picked up articles from previous days as I don’t refresh that feed often so they were added:today.

1 Like

I went looking for the Al weed vape that gives you Bitcoin for smoking

What a title :joy:

I don’t write 'em. I just record 'em :wink: :stuck_out_tongue:

1 Like

This is exactly what I was looking for! Thank you.

I was looking in the right place, but didn’t poke under annotations far enough and annotations was not what I would have thought would have been adjacent to my this task.

The script is rather helpful as well.

Now I need to add this to the window where I use pmset to wake my Mac in the morning and run my daily aggregation script and add it to my daily dump note from a chron job.

You’re welcome and many things are still better handled via traditional tools instead of AI / MCP.

And I’m glad the script is useful. It’s a bit more wordy than I’d write in private but it’s at least something to work from, if needed.