Get files from OpenFlow - NodRED

Hello all,

I am not sure if this is the right category to share this, but i have been walking in circles when figuring out how to best add files from OpenFlow to a Workitem in NodeRED.

I did however come up with a method, I am very curious to see other approaches, so please feel free to share :slight_smile:

I created this subflow in NodeRED to simplify things a bit.
this can be imported directly in NodeRED

Documentation

[
    {
        "id": "5eb293961cf0e7ac",
        "type": "subflow",
        "name": "Get files from OpenFlow",
        "info": "A subflow to get files from OpenFlow\r\n\r\n### Node-inputs\r\n\r\n_msg:_\r\n - `msg.uniquenames`\r\n\r\nAn array of strings containing the uniquenames you want to retireve\r\nfrom the files colleciton.\r\n - `msg.filetemplates`\r\n \r\nShould contain an array of filetemplate objects following this structure:\r\n\r\nmsg.filetemplates=\r\n`[`\r\n\r\n    {\"fileref\": \"exampleOne\",\r\n    \"uniquename\": \"example1-34h5435-3e454-4fa2-4534e-erwdf3t3.xlsx\"},\r\n\r\n    {\"fileref\": \"exampleTwo\",\r\n    \"uniquename\": \"example2-43fdew-8fgh-5fj9-fs2ag-01vs3.xlsx\"}\r\n`]`\r\n\r\n\r\n_env:_\r\n- `uniquename`\r\n\r\nPrimarily used to test and retrieve single static file\r\nfrom uniquename as string.\r\n\r\n**OBS: if provided flow will only use this**\r\n\r\n### Node-outputs\r\nExpected output given input\r\n\r\n - _uniquename_(env)\r\n\r\nSingle file object in an array\r\n\r\n` msg.files` =>[_fileObject_]\r\n\r\n - _msg.uniquenames_\r\n\r\nMultiple file object in an array\r\n\r\n `msg.files` => [_fileObject1_,_fileObject2_]\r\n\r\n  - _msg.filetemplates_\r\n\r\nReturns an object of objects based on the references given in the filetemplates for each file.\r\n\r\nExample based on input:\r\n\r\n  `msg.getfiles` =>\r\n `{`\r\n  \r\n    \"exampleOne\": {fileObject},\r\n    \r\n    \"exampleTwo\": {fileObject}\r\n  \r\n  `}`\r\n\r\nCan access each fileObjects using fileref from filetemplates:\r\n\r\n`msg.getfile`.exampleOne\r\n\r\n`msg.getfile`.exampleTwo\r\n\r\n\r\n\r\n\r\n",
        "category": "OpenFlow",
        "in": [
            {
                "x": 90,
                "y": 320,
                "wires": [
                    {
                        "id": "d96172cb3102a8a3"
                    }
                ]
            }
        ],
        "out": [
            {
                "x": 1480,
                "y": 500,
                "wires": [
                    {
                        "id": "7d2033ad96071bf3",
                        "port": 0
                    }
                ]
            }
        ],
        "env": [
            {
                "name": "uniquename",
                "type": "str",
                "value": "",
                "ui": {
                    "icon": "font-awesome/fa-file-o",
                    "type": "input",
                    "opts": {
                        "types": [
                            "str"
                        ]
                    }
                }
            }
        ],
        "meta": {},
        "color": "#3FADB5",
        "icon": "font-awesome/fa-angle-double-down",
        "status": {
            "x": 1320,
            "y": 580,
            "wires": [
                {
                    "id": "15cab6f14fcc7d79",
                    "port": 0
                },
                {
                    "id": "bd21494e3beb8f17",
                    "port": 0
                }
            ]
        }
    },
    {
        "id": "34b7c7445ddd9306",
        "type": "function",
        "z": "5eb293961cf0e7ac",
        "name": "Prepare query - files",
        "func": "let query = {\n  \"metadata.uniquename\": {\n    $in: msg.uniquenames\n  }\n};\n\nmsg.query = query;\n\nmsg.files = []\n\nreturn msg; // Return the modified msg object\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 630,
        "y": 380,
        "wires": [
            [
                "bd6c9a8e08be4bc4"
            ]
        ]
    },
    {
        "id": "bd6c9a8e08be4bc4",
        "type": "api get",
        "z": "5eb293961cf0e7ac",
        "name": "api get - files",
        "query": "query",
        "querytype": "msg",
        "projection": "",
        "projectiontype": "str",
        "top": 500,
        "toptype": "num",
        "skip": 0,
        "skiptype": "num",
        "collection": "files",
        "collectiontype": "str",
        "resultfield": "queryRes",
        "resultfieldtype": "msg",
        "orderby": "",
        "orderbytype": "str",
        "x": 660,
        "y": 440,
        "wires": [
            [
                "f17bc31e6315f665"
            ]
        ]
    },
    {
        "id": "fe0855616b828e34",
        "type": "function",
        "z": "5eb293961cf0e7ac",
        "name": "Fetch env variable",
        "func": "// Prepare object with file refs\nmsg.getfile = {}\n\n// Prepare list of filenames\nlet array_of_filenames = []\n\n// Fetch environment variables\nconst env_file_names = env.get(\"uniquename\")\n\n//If environment variable is provided use this, otherwise use msg.filetemplate if provided\nif (env_file_names !== \"\"){\n    array_of_filenames = [env_file_names]\n\n}\nelse{\n    if(typeof(msg.uniquenames) !== \"undefined\"){\n        array_of_filenames = msg.uniquenames\n    }\n    else{\n        if (typeof (msg.filetemplates) !== \"undefined\" && Array.isArray(msg.filetemplates)) {\n            // Use forEach to iterate over the array\n            msg.filetemplates.forEach(function(element) {\n                if (element.uniquename) {\n                    array_of_filenames.push(element.uniquename);\n                }\n            });\n        }\n        else{\n            node.error(\"Missing list of unique filenames\", msg);\n            msg.payload = {fill: \"red\", shape: \"dot\", text: \"FAILED\" }\n            return [msg, null]\n        }\n    }\n}\n\nmsg.uniquenames = array_of_filenames\n\nreturn [null, msg]",
        "outputs": 2,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 560,
        "y": 320,
        "wires": [
            [
                "b40f8b526665823b"
            ],
            [
                "34b7c7445ddd9306"
            ]
        ],
        "outputLabels": [
            "ERROR",
            "OK"
        ]
    },
    {
        "id": "bd21494e3beb8f17",
        "type": "function",
        "z": "5eb293961cf0e7ac",
        "name": "Analyse/prepare result",
        "func": "// Analysis feedback\nlet files_retrieved = msg.files.length\nlet files_requested = msg.uniquenames.length\nlet status_msg = \"Retrieved \" + files_retrieved + \"/\" + files_requested + \" files\"\n\nif(files_retrieved === files_requested){\n    msg.payload = {fill:\"green\",shape:\"ring\",text: status_msg}\n}\nelse if (files_retrieved === 0){\n    msg.payload = { fill: \"red\", shape: \"ring\", text: status_msg}\n}\nelse{\n    msg.payload = { fill: \"yellow\", shape: \"ring\", text: status_msg}\n}\n\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1150,
        "y": 540,
        "wires": [
            [
                "7d2033ad96071bf3"
            ]
        ]
    },
    {
        "id": "7df552f92f62234b",
        "type": "api download file",
        "z": "5eb293961cf0e7ac",
        "fileid": "file._id",
        "fileidtype": "msg",
        "filename": "file.filename",
        "filenametype": "msg",
        "result": "fileContent",
        "resulttype": "msg",
        "asbuffer": true,
        "name": "",
        "x": 900,
        "y": 560,
        "wires": [
            [
                "1959f835facdb4d7"
            ]
        ]
    },
    {
        "id": "1959f835facdb4d7",
        "type": "link out",
        "z": "5eb293961cf0e7ac",
        "name": "link out 38",
        "mode": "link",
        "links": [
            "98341b97fe65d372"
        ],
        "x": 1045,
        "y": 600,
        "wires": []
    },
    {
        "id": "98341b97fe65d372",
        "type": "link in",
        "z": "5eb293961cf0e7ac",
        "name": "link in 10",
        "links": [
            "1959f835facdb4d7"
        ],
        "x": 575,
        "y": 500,
        "wires": [
            [
                "f41d718560049fb7"
            ]
        ]
    },
    {
        "id": "f41d718560049fb7",
        "type": "function",
        "z": "5eb293961cf0e7ac",
        "name": "Populate files",
        "func": "// File content\nconst file_content = msg.fileContent\n\n// Remove from first not allowed character\nconst filename = msg.file.filename\n\nlet fileKey = filename\nmsg.files.push({\n    \"file\": file_content,\n    \"filename\": filename\n})\n\n// Find the object with the matching uniquename\nif(typeof(msg.filetemplates) !== \"undefined\"){\n    let matchingObject = msg.filetemplates.find(obj => obj.uniquename === msg.file.metadata.uniquename)\n    let refname = matchingObject.fileref\n\n    let value = {\n            \"name\": filename,\n            \"file\" : file_content\n    }\n\n    msg.getfile[refname] = value\n}\n\nreturn msg",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 690,
        "y": 500,
        "wires": [
            [
                "f17bc31e6315f665"
            ]
        ]
    },
    {
        "id": "f17bc31e6315f665",
        "type": "function",
        "z": "5eb293961cf0e7ac",
        "name": "Pop result",
        "func": "// pop file\nmsg.file = msg.queryRes.pop()\n\n// Exit if no file popped\nif(typeof(msg.file) === \"undefined\"){\n    return [msg, null]\n}\n\nreturn [null, msg];",
        "outputs": 2,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 870,
        "y": 500,
        "wires": [
            [
                "bd21494e3beb8f17"
            ],
            [
                "7df552f92f62234b"
            ]
        ],
        "outputLabels": [
            "Done",
            "Got file"
        ]
    },
    {
        "id": "d96172cb3102a8a3",
        "type": "function",
        "z": "5eb293961cf0e7ac",
        "name": "Status",
        "func": "msg.payload = { fill: \"blue\", shape: \"ring\", text: \"Running\" }\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 200,
        "y": 320,
        "wires": [
            [
                "d5dcb816bc9dedeb",
                "00774aff73059876"
            ]
        ]
    },
    {
        "id": "d5dcb816bc9dedeb",
        "type": "delay",
        "z": "5eb293961cf0e7ac",
        "name": "",
        "pauseType": "delay",
        "timeout": "1",
        "timeoutUnits": "seconds",
        "rate": "1",
        "nbRateUnits": "1",
        "rateUnits": "second",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": false,
        "allowrate": false,
        "outputs": 1,
        "x": 360,
        "y": 320,
        "wires": [
            [
                "fe0855616b828e34"
            ]
        ]
    },
    {
        "id": "00774aff73059876",
        "type": "link out",
        "z": "5eb293961cf0e7ac",
        "name": "link out 39",
        "mode": "link",
        "links": [
            "15cab6f14fcc7d79"
        ],
        "x": 325,
        "y": 260,
        "wires": []
    },
    {
        "id": "15cab6f14fcc7d79",
        "type": "link in",
        "z": "5eb293961cf0e7ac",
        "name": "link in 11",
        "links": [
            "00774aff73059876"
        ],
        "x": 1245,
        "y": 620,
        "wires": [
            []
        ]
    },
    {
        "id": "b40f8b526665823b",
        "type": "link out",
        "z": "5eb293961cf0e7ac",
        "name": "link out 40",
        "mode": "link",
        "links": [
            "ef64a2616fc46aea"
        ],
        "x": 705,
        "y": 280,
        "wires": []
    },
    {
        "id": "ef64a2616fc46aea",
        "type": "link in",
        "z": "5eb293961cf0e7ac",
        "name": "link in 12",
        "links": [
            "b40f8b526665823b"
        ],
        "x": 265,
        "y": 220,
        "wires": [
            [
                "00774aff73059876"
            ]
        ]
    },
    {
        "id": "7d2033ad96071bf3",
        "type": "function",
        "z": "5eb293961cf0e7ac",
        "name": "cleanup",
        "func": "// Cleanup\ndelete msg.payload.fill\ndelete msg.payload.shape\ndelete msg.payload.text\ndelete msg.payload.status\n\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1360,
        "y": 500,
        "wires": [
            []
        ]
    },
    {
        "id": "39e5a6a425601388",
        "type": "subflow:5eb293961cf0e7ac",
        "z": "5501d9bc7c2c1c8f",
        "name": "",
        "x": 610,
        "y": 4020,
        "wires": [
            [
                "16ff98fcc7d211d5"
            ]
        ]
    }
]
2 Likes

I found some issues with the initial subflow as it overwrites the payload inorder to update node status. here is an updated version to workaround this:

[
    {
        "id": "5eb293961cf0e7ac",
        "type": "subflow",
        "name": "Get files from OpenFlow",
        "info": "A subflow to get files from OpenFlow\r\n\r\n### Node-inputs\r\n\r\n_msg:_\r\n - `msg.uniquenames`\r\n\r\nAn array of strings containing the uniquenames you want to retireve\r\nfrom the files colleciton.\r\n - `msg.filetemplates`\r\n \r\nShould contain an array of filetemplate objects following this structure:\r\n\r\nmsg.filetemplates=\r\n`[`\r\n\r\n    {\"fileref\": \"exampleOne\",\r\n    \"uniquename\": \"example1-34h5435-3e454-4fa2-4534e-erwdf3t3.xlsx\"},\r\n\r\n    {\"fileref\": \"exampleTwo\",\r\n    \"uniquename\": \"example2-43fdew-8fgh-5fj9-fs2ag-01vs3.xlsx\"}\r\n`]`\r\n\r\n\r\n_env:_\r\n- `uniquename`\r\n\r\nPrimarily used to test and retrieve single static file\r\nfrom uniquename as string.\r\n\r\n**OBS: if provided flow will only use this**\r\n\r\n### Node-outputs\r\nExpected output given input\r\n\r\n - _uniquename_(env)\r\n\r\nSingle file object in an array\r\n\r\n` msg.files` =>[_fileObject_]\r\n\r\n - _msg.uniquenames_\r\n\r\nMultiple file object in an array\r\n\r\n `msg.files` => [_fileObject1_,_fileObject2_]\r\n\r\n  - _msg.filetemplates_\r\n\r\nReturns an object of objects based on the references given in the filetemplates for each file.\r\n\r\nExample based on input:\r\n\r\n  `msg.getfiles` =>\r\n `{`\r\n  \r\n    \"exampleOne\": {fileObject},\r\n    \r\n    \"exampleTwo\": {fileObject}\r\n  \r\n  `}`\r\n\r\nCan access each fileObjects using fileref from filetemplates:\r\n\r\n`msg.getfile`.exampleOne\r\n\r\n`msg.getfile`.exampleTwo\r\n\r\n\r\n\r\n\r\n",
        "category": "OpenFlow",
        "in": [
            {
                "x": 240,
                "y": 320,
                "wires": [
                    {
                        "id": "d5dcb816bc9dedeb"
                    },
                    {
                        "id": "d96172cb3102a8a3"
                    }
                ]
            }
        ],
        "out": [
            {
                "x": 1480,
                "y": 500,
                "wires": [
                    {
                        "id": "7d2033ad96071bf3",
                        "port": 0
                    }
                ]
            }
        ],
        "env": [
            {
                "name": "uniquename",
                "type": "str",
                "value": "",
                "ui": {
                    "icon": "font-awesome/fa-file-o",
                    "type": "input",
                    "opts": {
                        "types": [
                            "str"
                        ]
                    }
                }
            }
        ],
        "meta": {},
        "color": "#3FADB5",
        "icon": "font-awesome/fa-angle-double-down",
        "status": {
            "x": 1320,
            "y": 580,
            "wires": [
                {
                    "id": "15cab6f14fcc7d79",
                    "port": 0
                },
                {
                    "id": "bd21494e3beb8f17",
                    "port": 0
                }
            ]
        }
    },
    {
        "id": "34b7c7445ddd9306",
        "type": "function",
        "z": "5eb293961cf0e7ac",
        "name": "Prepare query - files",
        "func": "let query = {\n  \"metadata.uniquename\": {\n    $in: msg.uniquenames\n  }\n};\n\nmsg.query = query;\n\nmsg.files = []\n\nreturn msg; // Return the modified msg object\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 630,
        "y": 380,
        "wires": [
            [
                "bd6c9a8e08be4bc4"
            ]
        ]
    },
    {
        "id": "bd6c9a8e08be4bc4",
        "type": "api get",
        "z": "5eb293961cf0e7ac",
        "name": "api get - files",
        "query": "query",
        "querytype": "msg",
        "projection": "",
        "projectiontype": "str",
        "top": 500,
        "toptype": "num",
        "skip": 0,
        "skiptype": "num",
        "collection": "files",
        "collectiontype": "str",
        "resultfield": "queryRes",
        "resultfieldtype": "msg",
        "orderby": "",
        "orderbytype": "str",
        "x": 660,
        "y": 440,
        "wires": [
            [
                "f17bc31e6315f665"
            ]
        ]
    },
    {
        "id": "fe0855616b828e34",
        "type": "function",
        "z": "5eb293961cf0e7ac",
        "name": "Fetch env variable",
        "func": "// Prepare object with file refs\nmsg.getfile = {}\n\n// Prepare list of filenames\nlet array_of_filenames = []\n\n// Fetch environment variables\nconst env_file_names = env.get(\"uniquename\")\n\n//If environment variable is provided use this, otherwise use msg.filetemplate if provided\nif (env_file_names !== \"\"){\n    array_of_filenames = [env_file_names]\n\n}\nelse{\n    if(typeof(msg.uniquenames) !== \"undefined\"){\n        array_of_filenames = msg.uniquenames\n    }\n    else{\n        if (typeof (msg.filetemplates) !== \"undefined\" && Array.isArray(msg.filetemplates)) {\n            // Use forEach to iterate over the array\n            msg.filetemplates.forEach(function(element) {\n                if (element.uniquename) {\n                    array_of_filenames.push(element.uniquename);\n                }\n            });\n        }\n        else{\n            node.error(\"Missing list of unique filenames\", msg);\n            msg.payload = {fill: \"red\", shape: \"dot\", text: \"FAILED\" }\n            return [msg, null]\n        }\n    }\n}\n\nmsg.uniquenames = array_of_filenames\n\nreturn [null, msg]",
        "outputs": 2,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 560,
        "y": 320,
        "wires": [
            [
                "b40f8b526665823b"
            ],
            [
                "34b7c7445ddd9306"
            ]
        ],
        "outputLabels": [
            "ERROR",
            "OK"
        ]
    },
    {
        "id": "bd21494e3beb8f17",
        "type": "function",
        "z": "5eb293961cf0e7ac",
        "name": "Analyse/prepare result",
        "func": "// Analysis feedback\nlet files_retrieved = msg.files.length\nlet files_requested = msg.uniquenames.length\nlet status_msg = \"Retrieved \" + files_retrieved + \"/\" + files_requested + \" files\"\n\nif(files_retrieved === files_requested){\n    msg.payload = {fill:\"green\",shape:\"ring\",text: status_msg}\n}\nelse if (files_retrieved === 0){\n    msg.payload = { fill: \"red\", shape: \"ring\", text: status_msg}\n}\nelse{\n    msg.payload = { fill: \"yellow\", shape: \"ring\", text: status_msg}\n}\n\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1150,
        "y": 540,
        "wires": [
            [
                "7d2033ad96071bf3"
            ]
        ]
    },
    {
        "id": "7df552f92f62234b",
        "type": "api download file",
        "z": "5eb293961cf0e7ac",
        "fileid": "file._id",
        "fileidtype": "msg",
        "filename": "file.filename",
        "filenametype": "msg",
        "result": "fileContent",
        "resulttype": "msg",
        "asbuffer": true,
        "name": "",
        "x": 900,
        "y": 560,
        "wires": [
            [
                "1959f835facdb4d7"
            ]
        ]
    },
    {
        "id": "1959f835facdb4d7",
        "type": "link out",
        "z": "5eb293961cf0e7ac",
        "name": "link out 38",
        "mode": "link",
        "links": [
            "98341b97fe65d372"
        ],
        "x": 1045,
        "y": 600,
        "wires": []
    },
    {
        "id": "98341b97fe65d372",
        "type": "link in",
        "z": "5eb293961cf0e7ac",
        "name": "link in 10",
        "links": [
            "1959f835facdb4d7"
        ],
        "x": 575,
        "y": 500,
        "wires": [
            [
                "f41d718560049fb7"
            ]
        ]
    },
    {
        "id": "f41d718560049fb7",
        "type": "function",
        "z": "5eb293961cf0e7ac",
        "name": "Populate files",
        "func": "// File content\nconst file_content = msg.fileContent\n\n// Remove from first not allowed character\nconst filename = msg.file.filename\n\nlet fileKey = filename\nmsg.files.push({\n    \"file\": file_content,\n    \"filename\": filename\n})\n\n// Find the object with the matching uniquename\nif(typeof(msg.filetemplates) !== \"undefined\"){\n    let matchingObject = msg.filetemplates.find(obj => obj.uniquename === msg.file.metadata.uniquename)\n    let refname = matchingObject.fileref\n\n    let value = {\n            \"name\": filename,\n            \"file\" : file_content\n    }\n\n    msg.getfile[refname] = value\n}\n\nreturn msg",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 690,
        "y": 500,
        "wires": [
            [
                "f17bc31e6315f665"
            ]
        ]
    },
    {
        "id": "f17bc31e6315f665",
        "type": "function",
        "z": "5eb293961cf0e7ac",
        "name": "Pop result",
        "func": "// pop file\nmsg.file = msg.queryRes.pop()\n\n// Exit if no file popped\nif(typeof(msg.file) === \"undefined\"){\n    return [msg, null]\n}\n\nreturn [null, msg];",
        "outputs": 2,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 870,
        "y": 500,
        "wires": [
            [
                "bd21494e3beb8f17"
            ],
            [
                "7df552f92f62234b"
            ]
        ],
        "outputLabels": [
            "Done",
            "Got file"
        ]
    },
    {
        "id": "d5dcb816bc9dedeb",
        "type": "delay",
        "z": "5eb293961cf0e7ac",
        "name": "",
        "pauseType": "delay",
        "timeout": "1",
        "timeoutUnits": "seconds",
        "rate": "1",
        "nbRateUnits": "1",
        "rateUnits": "second",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": false,
        "allowrate": false,
        "outputs": 1,
        "x": 360,
        "y": 320,
        "wires": [
            [
                "fe0855616b828e34"
            ]
        ]
    },
    {
        "id": "00774aff73059876",
        "type": "link out",
        "z": "5eb293961cf0e7ac",
        "name": "link out 39",
        "mode": "link",
        "links": [
            "15cab6f14fcc7d79"
        ],
        "x": 455,
        "y": 260,
        "wires": []
    },
    {
        "id": "15cab6f14fcc7d79",
        "type": "link in",
        "z": "5eb293961cf0e7ac",
        "name": "link in 11",
        "links": [
            "00774aff73059876"
        ],
        "x": 1245,
        "y": 620,
        "wires": [
            []
        ]
    },
    {
        "id": "b40f8b526665823b",
        "type": "link out",
        "z": "5eb293961cf0e7ac",
        "name": "link out 40",
        "mode": "link",
        "links": [
            "ef64a2616fc46aea"
        ],
        "x": 705,
        "y": 280,
        "wires": []
    },
    {
        "id": "ef64a2616fc46aea",
        "type": "link in",
        "z": "5eb293961cf0e7ac",
        "name": "link in 12",
        "links": [
            "b40f8b526665823b"
        ],
        "x": 385,
        "y": 220,
        "wires": [
            [
                "00774aff73059876"
            ]
        ]
    },
    {
        "id": "7d2033ad96071bf3",
        "type": "function",
        "z": "5eb293961cf0e7ac",
        "name": "cleanup",
        "func": "// Cleanup\ndelete msg.payload.fill\ndelete msg.payload.shape\ndelete msg.payload.text\ndelete msg.payload.status\n\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1360,
        "y": 500,
        "wires": [
            []
        ]
    },
    {
        "id": "d96172cb3102a8a3",
        "type": "function",
        "z": "5eb293961cf0e7ac",
        "name": "Status",
        "func": "msg.payload = { fill: \"blue\", shape: \"ring\", text: \"Running\" }\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 350,
        "y": 280,
        "wires": [
            [
                "00774aff73059876"
            ]
        ]
    },
    {
        "id": "39e5a6a425601388",
        "type": "subflow:5eb293961cf0e7ac",
        "z": "5501d9bc7c2c1c8f",
        "name": "",
        "x": 610,
        "y": 4020,
        "wires": [
            [
                "16ff98fcc7d211d5"
            ]
        ]
    }
]