Notifications are stored in a SQLite database at this location: ~/Library/Group Containers/group.com.apple.usernoted/db2/db. Every new notification you receive on your Mac adds a new row to the record table in that SQLite database.
This means that if you provide BTT with Full Disk Access, then it's possible to write some JavaScript code to catch new notifications and then run any action you want. It works by assigning the JS script to a "Repeating or Time Based Trigger" Trigger with "Repeat every:" set to 5 seconds. All new notifications you receive will display the Title, Subtitle, Body and App Bundle ID in a dialog.
I understand your request to have a native "Notification Received" trigger in BTT, but I'm not sure what @Andreas_Hegenberg's strategy is regarding creating Triggers or Actions that require Full Disk Access, see:
In any case, here's the script that works as you described:
(async () => {
// Retrieve the stored 'rec_id' variable from BetterTouchTool's persistent storage.
// This value keeps track of the last notification ID that was recorded from:
// ~/Library/Group\ Containers/group.com.apple.usernoted/db2/db
let previousRecId = await get_number_variable({
variable_name: "previous_rec_id",
});
// If the 'previous_rec_id' variable is not defined (e.g., on the first run),
// initialize it to 0 to establish a starting point.
if (previousRecId === undefined) {
previousRecId = 0;
await set_persistent_number_variable({
variable_name: "previous_rec_id",
to: previousRecId,
});
}
// Define a shell command to obtain the 'rec_id' of the last recorded notification (where 'rec_id' is the largest)
// from the 'record' table in the ~/Library/Group\ Containers/group.com.apple.usernoted/db2/db database.
// This command uses sqlite3 to execute the query on the database and passes the --readonly flag as a safety measure to avoid
// inadvertently running a modified version of the command that would modify the database.
let previousRecIdShellScript =
'sqlite3 --readonly "$HOME/Library/Group Containers/group.com.apple.usernoted/db2/db" "SELECT rec_id FROM record ORDER BY rec_id DESC LIMIT 1;"';
// Execute the shell script and capture the current 'rec_id' of the last recorded notification.
let recId = await runShellScript({
script: previousRecIdShellScript,
});
// Check if a new notification has been received by comparing the current rec_id with the stored rec_id.
if (recId > previousRecId) {
// Update the persistent storage with the last rec_id.
await set_persistent_number_variable({
variable_name: "previous_rec_id",
to: recId,
});
// Define a shell command to retrieve the notification data for all of the notifications recorded since the last check.
let getNewNotificationsShellScript = `sqlite3 -json --readonly ~/Library/Group\\ Containers/group.com.apple.usernoted/db2/db \\
"SELECT HEX(data) as data FROM record WHERE rec_id > ${previousRecId} ORDER BY rec_id ASC;" \\
| python3 -c "import sys,json,plistlib,base64; \\
rows=json.load(sys.stdin); \\
norm=lambda o: {k: norm(v) for k,v in o.items()} if isinstance(o, dict) else [norm(x) for x in o] if isinstance(o, list) else base64.b64encode(o).decode('ascii') if isinstance(o,(bytes,bytearray)) else o.replace('\\n', '\\\\n').replace('\\r', '\\\\r') if isinstance(o, str) else o; \\
out=[norm(plistlib.loads(bytes.fromhex(r['data']))) for r in rows]; \\
print(json.dumps(out, indent=2))"`;
// Run the shell command to retrieve the notification data for all of the notifications recorded since the last check.
let newNotifications = await runShellScript({
script: getNewNotificationsShellScript,
});
// Convert the JSON string received from the shell command into a JavaScript object.
let newNotificationsObject = JSON.parse(newNotifications);
// Build a formatted string with all notification information
let notificationText = newNotificationsObject
.map((notification, index) => {
let app = notification.app || "Unknown App";
let title = notification.req?.titl || "";
let subtitle = notification.req?.subt || "";
let body = notification.req?.body || "";
let notificationInfo = `App: ${app}`;
if (title) notificationInfo += `\\nTitle: ${title}`;
if (subtitle) notificationInfo += `\\nSubtitle: ${subtitle}`;
if (body) notificationInfo += `\\nMessage: ${body}`;
return notificationInfo;
})
.join("\\n\\n" + "-".repeat(30) + "\\n\\n");
// Create AppleScript dialog with proper escaping for quotes
let escapedText = notificationText.replace(/"/g, '\\"');
let appleScript = `display dialog "${escapedText}" with title "New Notifications"`;
// Display the dialog using AppleScript
await runAppleScript(appleScript);
returnToBTT(newNotifications);
}
// Return the current 'rec_id' back to BetterTouchTool,
// useful for debugging and testing.
returnToBTT(recId);
})();
Here's the full Trigger + Action preset:
catch_macos_notifications.bttpreset (11.9 KB)
Here's the JSON of the full preset:
[
{
"BTTActionCategory" : 0,
"BTTLastUpdatedAt" : 1751234131.1894999,
"BTTTriggerType" : 678,
"BTTTriggerTypeDescriptionReadOnly" : "Repeating or Time Based Trigger",
"BTTTriggerClass" : "BTTTriggerTypeOtherTriggers",
"BTTUUID" : "5737250C-EAA0-4063-BE3C-34DE256E66A6",
"BTTPredefinedActionType" : 366,
"BTTPredefinedActionName" : "Empty Placeholder",
"BTTAdditionalConfiguration" : "{\"BTTTimedRepeatEveryXSeconds\":\"5\"}",
"BTTEnabled" : 1,
"BTTEnabled2" : 1,
"BTTOrder" : 2,
"BTTAdditionalActions" : [
{
"BTTActionCategory" : 0,
"BTTLastUpdatedAt" : 1751236674.2869849,
"BTTTriggerParentUUID" : "5737250C-EAA0-4063-BE3C-34DE256E66A6",
"BTTIsPureAction" : true,
"BTTTriggerClass" : "BTTTriggerTypeOtherTriggers",
"BTTUUID" : "1950EE79-D307-4C4E-8A45-EA64DFD5DE23",
"BTTPredefinedActionType" : 281,
"BTTPredefinedActionName" : "Run Real JavaScript",
"BTTAdditionalActionData" : {
"BTTScriptFunctionToCall" : "someJavaScriptFunction",
"BTTJavaScriptUseIsolatedContext" : false,
"BTTAppleScriptRunInBackground" : false,
"BTTScriptType" : 3,
"BTTAppleScriptString" : "(async () => {\n\t\/\/ Retrieve the stored 'rec_id' variable from BetterTouchTool's persistent storage.\n\t\/\/ This value keeps track of the last notification ID that was recorded from:\n\t\/\/ ~\/Library\/Group\\ Containers\/group.com.apple.usernoted\/db2\/db\n\tlet previousRecId = await get_number_variable({\n\t\tvariable_name: \"previous_rec_id\",\n\t});\n\n\t\/\/ If the 'previous_rec_id' variable is not defined (e.g., on the first run),\n\t\/\/ initialize it to 0 to establish a starting point.\n\tif (previousRecId === undefined) {\n\t\tpreviousRecId = 0;\n\t\tawait set_persistent_number_variable({\n\t\t\tvariable_name: \"previous_rec_id\",\n\t\t\tto: previousRecId,\n\t\t});\n\t}\n\n\t\/\/ Define a shell command to obtain the 'rec_id' of the last recorded notification (where 'rec_id' is the largest)\n\t\/\/ from the 'record' table in the ~\/Library\/Group\\ Containers\/group.com.apple.usernoted\/db2\/db database.\n\t\/\/ This command uses sqlite3 to execute the query on the database and passes the --readonly flag as a safety measure to avoid\n\t\/\/ inadvertently running a modified version of the command that would modify the database.\n\tlet previousRecIdShellScript =\n\t\t'sqlite3 --readonly \"$HOME\/Library\/Group Containers\/group.com.apple.usernoted\/db2\/db\" \"SELECT rec_id FROM record ORDER BY rec_id DESC LIMIT 1;\"';\n\n\t\/\/ Execute the shell script and capture the current 'rec_id' of the last recorded notification.\n\tlet recId = await runShellScript({\n\t\tscript: previousRecIdShellScript,\n\t});\n\n\t\/\/ Check if a new notification has been received by comparing the current rec_id with the stored rec_id.\n\tif (recId > previousRecId) {\n\t\t\/\/ Update the persistent storage with the last rec_id.\n\t\tawait set_persistent_number_variable({\n\t\t\tvariable_name: \"previous_rec_id\",\n\t\t\tto: recId,\n\t\t});\n\n\t\t\/\/ Define a shell command to retrieve the notification data for all of the notifications recorded since the last check.\n\t\tlet getNewNotificationsShellScript = `sqlite3 -json --readonly ~\/Library\/Group\\\\ Containers\/group.com.apple.usernoted\/db2\/db \\\\\n \"SELECT HEX(data) as data FROM record WHERE rec_id > ${previousRecId} ORDER BY rec_id ASC;\" \\\\\n| python3 -c \"import sys,json,plistlib,base64; \\\\\nrows=json.load(sys.stdin); \\\\\nnorm=lambda o: {k: norm(v) for k,v in o.items()} if isinstance(o, dict) else [norm(x) for x in o] if isinstance(o, list) else base64.b64encode(o).decode('ascii') if isinstance(o,(bytes,bytearray)) else o.replace('\\\\n', '\\\\\\\\n').replace('\\\\r', '\\\\\\\\r') if isinstance(o, str) else o; \\\\\nout=[norm(plistlib.loads(bytes.fromhex(r['data']))) for r in rows]; \\\\\nprint(json.dumps(out, indent=2))\"`;\n\n\t\t\/\/ Run the shell command to retrieve the notification data for all of the notifications recorded since the last check.\n\t\tlet newNotifications = await runShellScript({\n\t\t\tscript: getNewNotificationsShellScript,\n\t\t});\n\n\t\t\/\/ Convert the JSON string received from the shell command into a JavaScript object.\n\t\tlet newNotificationsObject = JSON.parse(newNotifications);\n\n\t\t\/\/ Build a formatted string with all notification information\n\t\tlet notificationText = newNotificationsObject\n\t\t\t.map((notification, index) => {\n\t\t\t\tlet app = notification.app || \"Unknown App\";\n\t\t\t\tlet title = notification.req?.titl || \"\";\n\t\t\t\tlet subtitle = notification.req?.subt || \"\";\n\t\t\t\tlet body = notification.req?.body || \"\";\n\n\t\t\t\tlet notificationInfo = `App: ${app}`;\n\t\t\t\tif (title) notificationInfo += `\\\\nTitle: ${title}`;\n\t\t\t\tif (subtitle) notificationInfo += `\\\\nSubtitle: ${subtitle}`;\n\t\t\t\tif (body) notificationInfo += `\\\\nMessage: ${body}`;\n\n\t\t\t\treturn notificationInfo;\n\t\t\t})\n\t\t\t.join(\"\\\\n\\\\n\" + \"-\".repeat(30) + \"\\\\n\\\\n\");\n\n\t\t\/\/ Create AppleScript dialog with proper escaping for quotes\n\t\tlet escapedText = notificationText.replace(\/\"\/g, '\\\\\"');\n\t\tlet appleScript = `display dialog \"${escapedText}\" with title \"New Notifications\"`;\n\n\t\t\/\/ Display the dialog using AppleScript\n\t\tawait runAppleScript(appleScript);\n\n\t\treturnToBTT(newNotifications);\n\t}\n\n\t\/\/ Return the current 'rec_id' back to BetterTouchTool,\n\t\/\/ useful for debugging and testing.\n\treturnToBTT(recId);\n})();\n",
"changedFile" : "4066057C-30C1-4D1E-92D7-01FD390A358E",
"BTTActionJSRunInSeparateContext" : false,
"BTTAppleScriptUsePath" : false,
"BTTScriptLocation" : 0
},
"BTTRealJavaScriptString" : "(async () => {\n\t\/\/ Retrieve the stored 'rec_id' variable from BetterTouchTool's persistent storage.\n\t\/\/ This value keeps track of the last notification ID that was recorded from:\n\t\/\/ ~\/Library\/Group\\ Containers\/group.com.apple.usernoted\/db2\/db\n\tlet previousRecId = await get_number_variable({\n\t\tvariable_name: \"previous_rec_id\",\n\t});\n\n\t\/\/ If the 'previous_rec_id' variable is not defined (e.g., on the first run),\n\t\/\/ initialize it to 0 to establish a starting point.\n\tif (previousRecId === undefined) {\n\t\tpreviousRecId = 0;\n\t\tawait set_persistent_number_variable({\n\t\t\tvariable_name: \"previous_rec_id\",\n\t\t\tto: previousRecId,\n\t\t});\n\t}\n\n\t\/\/ Define a shell command to obtain the 'rec_id' of the last recorded notification (where 'rec_id' is the largest)\n\t\/\/ from the 'record' table in the ~\/Library\/Group\\ Containers\/group.com.apple.usernoted\/db2\/db database.\n\t\/\/ This command uses sqlite3 to execute the query on the database and passes the --readonly flag as a safety measure to avoid\n\t\/\/ inadvertently running a modified version of the command that would modify the database.\n\tlet previousRecIdShellScript =\n\t\t'sqlite3 --readonly \"$HOME\/Library\/Group Containers\/group.com.apple.usernoted\/db2\/db\" \"SELECT rec_id FROM record ORDER BY rec_id DESC LIMIT 1;\"';\n\n\t\/\/ Execute the shell script and capture the current 'rec_id' of the last recorded notification.\n\tlet recId = await runShellScript({\n\t\tscript: previousRecIdShellScript,\n\t});\n\n\t\/\/ Check if a new notification has been received by comparing the current rec_id with the stored rec_id.\n\tif (recId > previousRecId) {\n\t\t\/\/ Update the persistent storage with the last rec_id.\n\t\tawait set_persistent_number_variable({\n\t\t\tvariable_name: \"previous_rec_id\",\n\t\t\tto: recId,\n\t\t});\n\n\t\t\/\/ Define a shell command to retrieve the notification data for all of the notifications recorded since the last check.\n\t\tlet getNewNotificationsShellScript = `sqlite3 -json --readonly ~\/Library\/Group\\\\ Containers\/group.com.apple.usernoted\/db2\/db \\\\\n \"SELECT HEX(data) as data FROM record WHERE rec_id > ${previousRecId} ORDER BY rec_id ASC;\" \\\\\n| python3 -c \"import sys,json,plistlib,base64; \\\\\nrows=json.load(sys.stdin); \\\\\nnorm=lambda o: {k: norm(v) for k,v in o.items()} if isinstance(o, dict) else [norm(x) for x in o] if isinstance(o, list) else base64.b64encode(o).decode('ascii') if isinstance(o,(bytes,bytearray)) else o.replace('\\\\n', '\\\\\\\\n').replace('\\\\r', '\\\\\\\\r') if isinstance(o, str) else o; \\\\\nout=[norm(plistlib.loads(bytes.fromhex(r['data']))) for r in rows]; \\\\\nprint(json.dumps(out, indent=2))\"`;\n\n\t\t\/\/ Run the shell command to retrieve the notification data for all of the notifications recorded since the last check.\n\t\tlet newNotifications = await runShellScript({\n\t\t\tscript: getNewNotificationsShellScript,\n\t\t});\n\n\t\t\/\/ Convert the JSON string received from the shell command into a JavaScript object.\n\t\tlet newNotificationsObject = JSON.parse(newNotifications);\n\n\t\t\/\/ Build a formatted string with all notification information\n\t\tlet notificationText = newNotificationsObject\n\t\t\t.map((notification, index) => {\n\t\t\t\tlet app = notification.app || \"Unknown App\";\n\t\t\t\tlet title = notification.req?.titl || \"\";\n\t\t\t\tlet subtitle = notification.req?.subt || \"\";\n\t\t\t\tlet body = notification.req?.body || \"\";\n\n\t\t\t\tlet notificationInfo = `App: ${app}`;\n\t\t\t\tif (title) notificationInfo += `\\\\nTitle: ${title}`;\n\t\t\t\tif (subtitle) notificationInfo += `\\\\nSubtitle: ${subtitle}`;\n\t\t\t\tif (body) notificationInfo += `\\\\nMessage: ${body}`;\n\n\t\t\t\treturn notificationInfo;\n\t\t\t})\n\t\t\t.join(\"\\\\n\\\\n\" + \"-\".repeat(30) + \"\\\\n\\\\n\");\n\n\t\t\/\/ Create AppleScript dialog with proper escaping for quotes\n\t\tlet escapedText = notificationText.replace(\/\"\/g, '\\\\\"');\n\t\tlet appleScript = `display dialog \"${escapedText}\" with title \"New Notifications\"`;\n\n\t\t\/\/ Display the dialog using AppleScript\n\t\tawait runAppleScript(appleScript);\n\n\t\treturnToBTT(newNotifications);\n\t}\n\n\t\/\/ Return the current 'rec_id' back to BetterTouchTool,\n\t\/\/ useful for debugging and testing.\n\treturnToBTT(recId);\n})();\n",
"BTTEnabled" : 1,
"BTTEnabled2" : 1,
"BTTOrder" : 796
},
{
"BTTActionCategory" : 0,
"BTTLastUpdatedAt" : 1751234127.3182611,
"BTTTriggerParentUUID" : "5737250C-EAA0-4063-BE3C-34DE256E66A6",
"BTTIsPureAction" : true,
"BTTTriggerClass" : "BTTTriggerTypeOtherTriggers",
"BTTUUID" : "84E122B3-10A9-4693-A96A-703FD54E39B2",
"BTTPredefinedActionType" : 254,
"BTTPredefinedActionName" : "Show HUD Overlay",
"BTTHUDActionConfiguration" : "{\"BTTActionHUDBlur\":true,\"BTTActionHUDBackground\":\"0.000000, 0.000000, 0.000000, 0.000000\",\"BTTIconConfigImageHeight\":100,\"BTTActionHUDPosition\":0,\"BTTActionHUDDetail\":\"\",\"BTTActionHUDDuration\":2,\"BTTActionHUDDisplayToUse\":0,\"BTTIconConfigImageWidth\":100,\"BTTActionHUDSlideDirection\":0,\"BTTActionHUDHideWhenOtherHUDAppears\":false,\"BTTActionHUDWidth\":220,\"BTTActionHUDAttributedTitle\":\"{\\\\rtf1\\\\ansi\\\\ansicpg1252\\\\cocoartf2822\\n\\\\cocoatextscaling0\\\\cocoaplatform0{\\\\fonttbl\\\\f0\\\\fnil\\\\fcharset0 SFPro-Bold;\\\\f1\\\\fswiss\\\\fcharset0 Helvetica;\\\\f2\\\\fnil\\\\fcharset0 SFPro-Regular;\\n}\\n{\\\\colortbl;\\\\red255\\\\green255\\\\blue255;\\\\red0\\\\green0\\\\blue0;}\\n{\\\\*\\\\expandedcolortbl;;\\\\cssrgb\\\\c0\\\\c0\\\\c0\\\\c84706\\\\cname labelColor;}\\n\\\\pard\\\\tx560\\\\tx1120\\\\tx1680\\\\tx2240\\\\tx2800\\\\tx3360\\\\tx3920\\\\tx4480\\\\tx5040\\\\tx5600\\\\tx6160\\\\tx6720\\\\pardirnatural\\\\qc\\\\partightenfactor0\\n\\n\\\\f0\\\\b\\\\fs80 \\\\cf2 Ran\\n\\\\f1\\\\b0\\\\fs24 \\\\\\n\\\\pard\\\\tx560\\\\tx1120\\\\tx1680\\\\tx2240\\\\tx2800\\\\tx3360\\\\tx3920\\\\tx4480\\\\tx5040\\\\tx5600\\\\tx6160\\\\tx6720\\\\pardirnatural\\\\qc\\\\partightenfactor0\\n\\n\\\\f2\\\\fs48 \\\\cf2 \\\\{previous_rec_id\\\\}}\",\"BTTActionHUDBorderWidth\":0,\"BTTActionHUDTitle\":\"\",\"BTTActionHUDHeight\":220}",
"BTTEnabled" : 1,
"BTTEnabled2" : 0,
"BTTOrder" : 797
}
]
}
]