Dynamic messages into an iOS actionable notifications subflow

I’ve been using this subflow for actionable notifications in Node-RED… or, something similar. I’m having trouble passing dynamic message content into the flow such that it makes it into the notification. Here’s the subflow:

[{"id":"35055683ae253203","type":"function","z":"67e867a7844ca017","name":"create service call","func":"msg._originalPayload = msg.payload;\n\nconst now = Date.now();\nconst getEnv = key => env.get(key) || ''; // Helper to fetch env values\nconst safeString = str => str.replace(/[^\\w\\s]/gi, '').replace(/\\s+/g, '_').toUpperCase(); // Sanitization\n\nlet flow_msg_variables = {\n  tag: '',\n  service: '',\n  message: msg,\n  date_created: now\n}\n\n// Extract overrides or fallback to env values\nconst override = msg.notificationOverride ?? {};\nconst xTitle = override.title ?? getEnv('title');\nconst xSubtitle = override.subtitle ?? getEnv('subtitle');\nconst xMessage = override.message ?? getEnv('message');\nconst xUrl = override.url ?? getEnv('notificationUrl');\nconst xServices = override.services ?? getEnv('service');\nconst xCameraEntity = override.cameraEntity ?? getEnv('cameraEntity');\nconst xInterruptionLevel = override.interruptionLevel ?? getEnv('interruptionLevel');\nconst xCustomSound = override.customSound ?? getEnv('customSound');\nconst xGroup = override.group ?? getEnv('group');\n// Map Information\nconst xLatitudeFirst = override.latitudeFirst ?? getEnv('latitudeFirst');\nconst xLongitudeFirst = override.longitudeFirst ?? getEnv('longitudeFirst');\nconst xLatitudeSecond = override.latitudeSecond ?? getEnv('latitudeSecond');\nconst xLongitudeSecond = override.longitudeSecond ?? getEnv('longitudeSecond');\n// Media Information\nconst xContentUrl = override.contentUrl ?? getEnv('contentUrl');\nconst xImagePath = override.imagePath ?? getEnv('imagePath');\nconst xVideoPath = override.videoPath ?? getEnv('videoPath');\nconst xAudioPath = override.audioPath ?? getEnv('audioPath');\nconst xLazyLoading = override.lazyLoading ?? getEnv('lazyLoading');\nconst xHideThumbnail = override.hideThumbnail ?? getEnv('hideThumbnail');\n\n\n// Tag setup\nflow_msg_variables.tag = safeString(override.tag ?? getEnv('tag')) || `${safeString(getEnv('title'))}_${flow.get('random')}`;\nflow_msg_variables.service = xServices;\n    \n// Process messages array\nlet all_flow_messages = flow.get('flow_messages') || [];\nlet new_flow_messages = all_flow_messages.filter(msg => \n    !(msg.tag === flow_msg_variables.tag && msg.service === flow_msg_variables.service) &&\n    (now - msg.date_created < 86400000) // keep messages less than 24h old\n);\n\nnew_flow_messages.push(flow_msg_variables);\nflow.set('flow_messages',new_flow_messages);\n\n// Services check\nif (!xServices.trim()) {\n    node.status({ text: 'no services defined', shape: 'ring', fill: 'red' });\n    return;\n} else if (xServices.trim() === \"NOONE\") {\n    node.status({ text: 'No one to send to', shape: 'ring', fill: 'yellow' });\n    return;\n}\n\n// Actions setup\nconst actions = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(i => {\n    const title = override[`action${i}Title`] ?? getEnv(`action${i}Title`);\n    if (!title) return null;\n\n    const actionObj = {\n        action: safeString(title),\n        title\n    };\n\n    // Conditionally add properties if they are not empty\n    const properties = [\n        'activationMode', 'uri', 'textInputButtonTitle',\n        'textInputPlaceholder', 'authenticationRequired',\n        'destructive', 'behavior', 'icon'\n    ];\n\n    properties.forEach(prop => {\n        const value = override[`action${i}${prop.charAt(0).toUpperCase() + prop.slice(1)}`] ?? getEnv(`action${i}${prop.charAt(0).toUpperCase() + prop.slice(1)}`);\n        if (value) {\n            actionObj[prop] = value;\n        }\n    });\n\n    return actionObj;\n}).filter(Boolean);\n\n\n// Create msg object\nmsg.payload = {\n    data: {\n        title: xTitle,\n        message: xMessage,\n        data: {\n            tag: flow_msg_variables.tag,\n            ...(actions.length > 0 && {actions}),\n            ...(actions.length > 0 && {\n                action_data: { tag: flow_msg_variables.tag }\n            }),\n            ...(xUrl && { url: xUrl }),\n            ...(xSubtitle && { subtitle: xSubtitle }),\n            ...(xCameraEntity && { entity_id: xCameraEntity }),\n            ...(xGroup && { group: xGroup }),\n            push: {\n                sound: {\n                    name: xCustomSound || getEnv('customSoundPreInstalled') || 'default',\n                    ...(env.get('isCriticalNotification') && { critical: 1, volume: 1.0 })\n                },\n                ...(xInterruptionLevel && { \"interruption-level\": xInterruptionLevel })\n            }\n        }\n    }\n};\n\n// Map Information\nif (xLatitudeFirst && xLongitudeFirst) {\n    msg.payload.data.data.action_data = {\n        ...(actions.length > 0 && { tag: flow_msg_variables.tag }),\n        latitude: xLatitudeFirst,\n        longitude: xLongitudeFirst,\n        ...(xLatitudeSecond && xLongitudeSecond && {\n            second_latitude: xLatitudeSecond,\n            second_longitude: xLongitudeSecond,\n            shows_line_between_points: override.showLineBetweenPoints ?? getEnv('showLineBetweenPoints'),\n            shows_compass: override.showCompass ?? getEnv('showCompass'),\n            shows_points_of_interest: override.showPointsOfInterest ?? getEnv('showPointsOfInterest'),\n            shows_scale: override.showScale ?? getEnv('showScale'),\n            shows_traffic: override.showTraffic ?? getEnv('showTraffic'),\n            shows_user_location: override.showUserLocation ?? getEnv('showUserLocation')\n        })\n    };\n}\n\n// Media Information\nif (xContentUrl || xImagePath || xVideoPath || xAudioPath) {\n    msg.payload.data.data = {\n        ...msg.payload.data.data,\n        contentUrl: xContentUrl || undefined,\n        image: xImagePath || undefined,\n        video: xVideoPath || undefined,\n        audio: xAudioPath || undefined,\n        lazy: xLazyLoading || undefined,\n        attachment: xHideThumbnail ? { 'hide-thumbnail': xHideThumbnail } : undefined\n    };\n}\n\n// Send notifications\nxServices.trim().split(/,\\s*/).forEach(service => {\n    if (!service) return;\n    msg.payload.action = `notify.${service}`;\n    node.send(msg);\n});\n\nnode.done();","outputs":1,"timeout":"","noerr":0,"initialize":"flow.set('random',Math.random().toString(36).replace(/[^a-z]+/g, '').slice(0, 5).toUpperCase());","finalize":"","libs":[],"x":430,"y":80,"wires":[["7636c6f3fe2966be","25cd950f148bdb21"]]},{"id":"1d36a2652523a737","type":"switch","z":"67e867a7844ca017","name":"which action?","property":"responseIndex","propertyType":"flow","rules":[{"t":"eq","v":"1","vt":"num"},{"t":"eq","v":"2","vt":"num"},{"t":"eq","v":"3","vt":"num"},{"t":"eq","v":"4","vt":"num"}],"checkall":"true","repair":false,"outputs":4,"x":1280,"y":480,"wires":[[],[],[],[]]},{"id":"144460bb41facc58","type":"status","z":"67e867a7844ca017","name":"","scope":["35055683ae253203","e21fb9e4eee3a768","b57a6c93227eba63","c3e63297b4ba36b2"],"x":100,"y":320,"wires":[[]]},{"id":"e21fb9e4eee3a768","type":"function","z":"67e867a7844ca017","name":"build message","func":"const latestMessage = flow.get('latestMessage');\nconst event = msg.payload.event;\n\nconst getEnv = key => env.get(key) || ''; // Helper to fetch env values\nconst safeString = str => str.replace(/[^\\w\\s]/gi, '').replace(/\\s+/g, '_').toUpperCase(); // Sanitization\n\n// Extract overrides or fallback to env values\nconst override = latestMessage.notificationOverride ?? {};\n\nlatestMessage.payload = latestMessage._originalPayload;\nlatestMessage.eventData = msg.payload;\ndelete latestMessage._originalPayload;\n\nif (env.get('userInfo')) {\n    latestMessage.userData = msg.userData.find(u => u.id === msg.payload.context.user_id);\n}\n\nconst actionNames = [1, 2, 3, 4].map(i => {\n    const title = override[`action${i}Title`] ?? getEnv(`action${i}Title`);\n    return safeString(title);\n});\n\nlatestMessage.actionNames = actionNames;\n\nconst index = actionNames.findIndex(name => name === event.actionName) + 1;\n\nif (index) {\n    flow.set(\"responseIndex\", index);\n    node.status({\n        text: `${actionNames[index - 1]} at: ${getPrettyDate()}`,\n        shape: 'dot',\n        fill: 'green'\n    });\n}\n\nreturn latestMessage;\n\nfunction getPrettyDate() {\n    return new Date().toLocaleDateString('en-US', {\n        month: 'short',\n        day: 'numeric',\n        hour12: false,\n        hour: 'numeric',\n        minute: 'numeric',\n    });\n}\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1080,"y":480,"wires":[["1d36a2652523a737","4c98d1addbc534b6"]]},{"id":"808cf2c07196978a","type":"ha-api","z":"67e867a7844ca017","name":"get user info","server":"296c0678.b5f9ca","version":1,"debugenabled":false,"protocol":"websocket","method":"get","path":"","data":"{\"type\": \"config/auth/list\"}","dataType":"json","responseType":"json","outputProperties":[{"property":"userData","propertyType":"msg","value":"","valueType":"results"}],"x":950,"y":420,"wires":[["e21fb9e4eee3a768"]]},{"id":"449ff3ce4d1a7ca8","type":"switch","z":"67e867a7844ca017","name":"fetch user info?","property":"userInfo","propertyType":"env","rules":[{"t":"true"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":800,"y":480,"wires":[["808cf2c07196978a"],["e21fb9e4eee3a768"]]},{"id":"35a428ec0772074d","type":"server-events","z":"67e867a7844ca017","name":"ios.notification_action_fired","server":"296c0678.b5f9ca","version":3,"exposeAsEntityConfig":"","eventType":"ios.notification_action_fired","eventData":"","waitForRunning":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"$outputData(\"eventData\").event_type","valueType":"jsonata"},{"property":"event_type","propertyType":"msg","value":"$outputData(\"eventData\").event_type","valueType":"jsonata"}],"x":164,"y":256,"wires":[["a0b4e6bc9c85191e"]]},{"id":"b57a6c93227eba63","type":"api-call-service","z":"67e867a7844ca017","name":"Send Notifications","server":"296c0678.b5f9ca","version":7,"debugenabled":false,"action":"","floorId":[],"areaId":[],"deviceId":[],"entityId":[],"labelId":[],"data":"","dataType":"json","mergeContext":"callServiceData","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":false,"domain":"notify","service":"","x":830,"y":80,"wires":[[]]},{"id":"c3e63297b4ba36b2","type":"function","z":"67e867a7844ca017","name":"create CLEAR service call","func":"msg._originalPayload = msg.payload;\n\nlet tag_to_clear = \"\";\nlet services = \"\";\nlet clearAll = false;\n\nif (msg.notificationOverride) {\n    clearAll = !!msg.notificationOverride.clear;\n    services = msg.notificationOverride.services || \"\";\n    tag_to_clear = msg.notificationOverride.tag\n        ? msg.notificationOverride.tag.replace(/[^\\w\\s]/gi, '').replace(/\\s+/g, '_').toUpperCase()\n        : \"\";\n}\n\nif (!tag_to_clear) { // No specific tag sent, clear the last message.\n    let all_flow_messages = flow.get('flow_messages');\n    if (all_flow_messages && all_flow_messages.length > 0) {\n        let last_message = all_flow_messages.pop();\n        tag_to_clear = last_message.tag;\n        services = last_message.service;\n        flow.set('flow_messages', all_flow_messages);\n    }\n}\n\nif (!services) {\n    node.status({\n        text: 'no services defined',\n        shape: 'ring',\n        fill: 'red'\n    });\n    return;\n}\n\nif (!tag_to_clear) {\n    node.status({\n        text: 'no messages to delete',\n        shape: 'ring',\n        fill: 'red'\n    });\n    return;\n}\n\n// Create iOS msg object\nmsg.payload = {\n    data: {\n        message: \"clear_notification\",\n        data: {\n            tag: tag_to_clear\n        }\n    }\n};\n\nfunction getPrettyDate() {\n    return new Date().toLocaleDateString('en-US', {\n        month: 'short',\n        day: 'numeric',\n        hour12: false,\n        hour: 'numeric',\n        minute: 'numeric',\n    });\n}\n\nif (clearAll) {\n    delete msg.notificationOverride;\n}\n\nlet xCountCleared = 0;\nservices.trim().split(/,\\s*/).forEach(service => {\n    if (!service) return;\n\n    if (clearAll || !service.includes(msg._originalPayload.event.sourceDeviceID)) {\n        msg.payload.action = `notify.${service}`;\n        node.send(msg);\n        xCountCleared++;\n    }\n});\n\nnode.status({\n    text: `${xCountCleared > 0 ? `${xCountCleared} messages cleared` : 'No messages cleared'} at: ${getPrettyDate()}`,\n    shape: 'dot',\n    fill: xCountCleared > 0 ? 'blue' : 'red'\n});\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":730,"y":300,"wires":[["6e891f0aa83ce24b","467014ff139a7f81"]]},{"id":"a6ef52fc22ff6572","type":"switch","z":"67e867a7844ca017","name":"Clear Notification on Action?","property":"isClearNotificationsOnAction","propertyType":"env","rules":[{"t":"true"},{"t":"false"}],"checkall":"true","repair":false,"outputs":2,"x":460,"y":440,"wires":[["c3e63297b4ba36b2","4ab47be24d35236a"],["449ff3ce4d1a7ca8"]]},{"id":"4ff24e3880801123","type":"api-call-service","z":"67e867a7844ca017","name":"Clear Notifications","server":"296c0678.b5f9ca","version":7,"debugenabled":false,"action":"","floorId":[],"areaId":[],"deviceId":[],"entityId":[],"labelId":[],"data":"","dataType":"json","mergeContext":"callServiceData","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":false,"domain":"notify","service":"","x":1210,"y":240,"wires":[[]]},{"id":"4ab47be24d35236a","type":"delay","z":"67e867a7844ca017","name":"","pauseType":"delay","timeout":"10","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":720,"y":420,"wires":[["449ff3ce4d1a7ca8"]]},{"id":"e91478eee0eb39da","type":"switch","z":"67e867a7844ca017","name":"clear?","property":"notificationOverride.clear","propertyType":"msg","rules":[{"t":"istype","v":"undefined","vt":"undefined"},{"t":"null"},{"t":"false"},{"t":"true"}],"checkall":"false","repair":false,"outputs":4,"x":210,"y":80,"wires":[["35055683ae253203"],["35055683ae253203"],["35055683ae253203"],["c3e63297b4ba36b2"]]},{"id":"6e891f0aa83ce24b","type":"delay","z":"67e867a7844ca017","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":990,"y":240,"wires":[["4ff24e3880801123"]]},{"id":"7636c6f3fe2966be","type":"delay","z":"67e867a7844ca017","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"outputs":1,"x":630,"y":80,"wires":[["b57a6c93227eba63"]]},{"id":"a0b4e6bc9c85191e","type":"function","z":"67e867a7844ca017","name":"belongs here?","func":"let msg_tag = msg.payload.event.action_data.tag.trim();\nlet all_flow_messages = flow.get('flow_messages');\n\nif (all_flow_messages) {\n    let matchedMessage = all_flow_messages.find(\n        message => message.tag.trim() === msg_tag\n    );\n    if (matchedMessage) {\n        flow.set('latestMessage', matchedMessage.message);\n        return [msg, null];\n    }\n}\n\nreturn [null, msg];","outputs":2,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":400,"y":260,"wires":[["a6ef52fc22ff6572"],[]]},{"id":"f443cc500f68294f","type":"debug","z":"67e867a7844ca017","name":"iOS Notify Debug Input Message","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":200,"y":200,"wires":[]},{"id":"9e2c1eb9fcd4891d","type":"debug","z":"67e867a7844ca017","name":"iOS Notify Debug Notify Service Call","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":870,"y":160,"wires":[]},{"id":"128f1b2bd0699e31","type":"debug","z":"67e867a7844ca017","name":"iOS Notify Debug Clear Service Call","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1270,"y":300,"wires":[]},{"id":"4ac9e496e3ab0de9","type":"debug","z":"67e867a7844ca017","name":"iOS Notify Debug Output Message","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1120,"y":600,"wires":[]},{"id":"28115a32dc285e97","type":"switch","z":"67e867a7844ca017","name":"Debug?","property":"debugMode","propertyType":"env","rules":[{"t":"true"}],"checkall":"true","repair":false,"outputs":1,"x":140,"y":140,"wires":[["f443cc500f68294f"]]},{"id":"4c98d1addbc534b6","type":"switch","z":"67e867a7844ca017","name":"Debug?","property":"debugMode","propertyType":"env","rules":[{"t":"true"}],"checkall":"true","repair":false,"outputs":1,"x":1040,"y":540,"wires":[["4ac9e496e3ab0de9"]]},{"id":"467014ff139a7f81","type":"switch","z":"67e867a7844ca017","name":"Debug?","property":"debugMode","propertyType":"env","rules":[{"t":"true"}],"checkall":"true","repair":false,"outputs":1,"x":980,"y":300,"wires":[["128f1b2bd0699e31"]]},{"id":"25cd950f148bdb21","type":"switch","z":"67e867a7844ca017","name":"Debug?","property":"debugMode","propertyType":"env","rules":[{"t":"true"}],"checkall":"true","repair":false,"outputs":1,"x":620,"y":160,"wires":[["9e2c1eb9fcd4891d"]]},{"id":"296c0678.b5f9ca","type":"server","name":"Home Assistant","version":5,"addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true,"heartbeat":false,"heartbeatInterval":30,"areaSelector":"friendlyName","deviceSelector":"friendlyName","entitySelector":"friendlyName","statusSeparator":"at: ","statusYear":"hidden","statusMonth":"short","statusDay":"numeric","statusHourCycle":"h23","statusTimeFormat":"h:m","enableGlobalContextStore":true}]

In the message field of the actionable notification, I’ll put {{{payload}}}. That works for simple notify actions in action nodes, but not in this subflow. Any ideas that might help?

Thanks.

Mustache templates are not advised for complex data, use jsonata instead.

Sorry, subflows don’t normally accept mustache templates (or JSONata expressions).

Nothing quite as useful as reading the manual first though, where in this case you will find the answer you are looking for.

When using the iOS Actionable Notification node, you can dynamically override various properties by setting the msg.notificationOverride object. Below is a complete sample that demonstrates how to override each possible value, including the newly added properties.

Sample Code for Overrides
// Set msg overrides
### Documentation Sample for Override Properties
msg.notificationOverride = {};

// Basic Notification Information
msg.notificationOverride.title = "Dynamic Title!";
msg.notificationOverride.subtitle = "Dynamic Subtitle";
msg.notificationOverride.message = "Battery is at " + msg.payload.attributes.battery + "%!! Replace it
2 Likes

Ahhh, overrides. Thanks–tested and working.