Howto: Call a NodeRed flow from a HA script with "parameters" and return data to the script

Hi there,

I normally use Node-RED for all my automations in HA.
But lately I needed a lot of scripts, as I write tools for my LLM-based voice assist in HA (which can use exposed scripts as tools).

But as scripts are limited and you can’t handle every complicated task (access REST endpoints, query dynamically SQL statements from databases, …) I was looking in triggering Node-RED flows from a script and wait for the data returned by Node-RED.

After writing a few very similar scripts / flows I decided to refactor that into a generic script that can be reused and called by other scripts (in my case the different specific LLM tool scripts).

Maybe this is useful for others too. :slight_smile:

This is the generic script, just paste it to Settings → Automations & Scenes → Scripts:

alias: Node-RED Request
description: >-
  Generic script to call a Node-RED flow -> Wait for Result -> Return as
  response.


  Uses an unique ID for each event when passing data to Node-RED.

  Waits for a response with the exact same ID and returns the response data, or
  an error in case of timeout.


  That way multiple calls can be made in parallel and each instance will get the
  correct response regardless of request duration and order. 
mode: parallel
max: 20
fields:
  channel:
    description: >-
      Flow identifier. Use In Node-Red (switch node) to decide if the event is
      for your current flow or should be ignored.
    example: my-node-red-flow-identifier
    required: true
    selector:
      text: {}
  payload:
    description: >-
      Object to provide parameters and data to the flow. Will be availabe
      through the event data in Node-RED.
    example: >
      { "entity_id": "sensor.name_of_sensor", "data": { "action":
      "do_something"} }
    selector:
      object: {}
  timeout:
    description: Timeout in seconds until an error will be returned
    required: true
    default: "10"
    selector:
      number:
        min: 0
        max: 600
sequence:
  - variables:
      _req: "{{ 'req_' ~ now().timestamp() | int ~ '_' ~ range(1000,9999)|random }}"
      _channel: "{{ channel }}"
      _payload: "{{ payload | default({}, true) }}"
  - event: nodered_request.trigger
    event_data:
      channel: "{{ _channel }}"
      request_id: "{{ _req }}"
      payload: "{{ _payload }}"
  - wait_for_trigger:
      - event_type: nodered_request.response
        event_data:
          channel: "{{ _channel }}"
          request_id: "{{ _req }}"
        trigger: event
    timeout:
      seconds: "{{ timeout }}"
  - choose:
      - conditions:
          - condition: template
            value_template: "{{ wait.completed }}"
        sequence:
          - variables:
              response:
                result: "{{ wait.trigger.event.data.result | default(None) }}"
                error: "{{ wait.trigger.event.data.error | default(None) }}"
    default:
      - variables:
          response:
            result: null
            error:
              code: timeout
              message: No answer within {{ timeout }} seconds.
  - stop: ""
    response_variable: response

And here is an example flow to catch the triggering event and send back a response:


[{"id":"39e2fe3bbdb749b2","type":"group","z":"64f4dc3ef013c268","name":"Sample flow for \"Node-RED Request\" script","style":{"label":false,"stroke":"none","fill":"#d1d1d1","fill-opacity":"0.5"},"nodes":["941395bc8d0c662e","e672de2c700fcbe5","0435f612e7c930af","687bb6fa2765cc5e","0f763b21a2ea8fd9","db8d6d2a9142a672"],"x":14,"y":319,"w":1052,"h":142},{"id":"941395bc8d0c662e","type":"server-events","z":"64f4dc3ef013c268","g":"39e2fe3bbdb749b2","name":"","server":"ef6aa0b.3fe4a6","version":3,"exposeAsEntityConfig":"","eventType":"nodered_request.trigger","eventData":"","waitForRunning":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"$outputData(\"eventData\").event_type","valueType":"jsonata"}],"x":170,"y":420,"wires":[["687bb6fa2765cc5e"]]},{"id":"e672de2c700fcbe5","type":"ha-fire-event","z":"64f4dc3ef013c268","g":"39e2fe3bbdb749b2","name":"","server":"ef6aa0b.3fe4a6","version":0,"event":"nodered_request.response","data":"","dataType":"jsonata","x":900,"y":420,"wires":[[]]},{"id":"0435f612e7c930af","type":"function","z":"64f4dc3ef013c268","g":"39e2fe3bbdb749b2","name":"Create Response","func":"const event = msg.payload.event;\n\nif (!event.channel || !event.request_id) {\n    node.warn('Missing channel or request_id on incoming event');\n    return null;\n}\n\nmsg = { \n    payload : {\n        data: {\n            channel: event.channel,\n            request_id: event.request_id,\n            result: {\n                return_value: {\n                    status: \"success\",\n                    message: \"whoohoo, we received this text: \" + event.payload\n                }\n            }\n        } \n    }\n};\n\n// In case of error, set msg.payload.error.code and message\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":650,"y":420,"wires":[["e672de2c700fcbe5"]]},{"id":"687bb6fa2765cc5e","type":"switch","z":"64f4dc3ef013c268","g":"39e2fe3bbdb749b2","name":"Are we the receiver?","property":"payload.event.channel","propertyType":"msg","rules":[{"t":"eq","v":"test-flow","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":420,"y":420,"wires":[["0435f612e7c930af","db8d6d2a9142a672"]]},{"id":"0f763b21a2ea8fd9","type":"comment","z":"64f4dc3ef013c268","g":"39e2fe3bbdb749b2","name":"Sample flow for \"Node-RED Request\" script","info":"","x":210,"y":360,"wires":[]},{"id":"db8d6d2a9142a672","type":"debug","z":"64f4dc3ef013c268","g":"39e2fe3bbdb749b2","name":"Event Data","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload.event","targetType":"msg","statusVal":"","statusType":"auto","x":630,"y":360,"wires":[]},{"id":"ef6aa0b.3fe4a6","type":"server","name":"Home Assistant","addon":true}]

And as final piece, a matching sample script that calls the “Node-RED Request” script with the correct flow identifier for the provided sample flow.

alias: Node-RED Test
description: |
  Simple test script, to show how to call the 'Node-RED Request' script
mode: single
fields:
  text:
    name: Text
    description: Text to send to Node-Red flow
    required: true
    selector:
      text: {}
sequence:
  - action: script.node_red_request
    data:
      channel: test-flow
      payload: "{{ text }}"
      timeout: 5
    response_variable: response
  - variables:
      result: "{{ {'value': response | default(None)} }}"
  - stop: ""
    response_variable: result

Which in the end looks simply like that when called from the developers tools: