JSONata $toMillis() in change node will not convert an ISO complaint date string

I have another anomaly when using JSONata in a change node, and it is to do with time/date objects (of course).
I am using a node-red-contrib-sun-event-trigger to produce a sunrise trigger, and also capture the days other sun related events. on start up it produces an object with several key:value pairs for the days event, with the value being a date string like "2026-05-12T20:48:34.671Z" which is ISO compatible. But when I try to convert this to a date number with $toMillis() in a change node I get the error.

Invalid JSONata expression: Argument 1 of function "toMillis" does not match function signature

The Jsonata statement works in try.json.org when tested against a copy of the data fed to the node.

I can convert it the date string with some JS in a function node (val is the date string)

    else if (isValidDateString(val)) {
        // If it's a valid date string, parse directly
        millis = new Date(val.trim()).getTime();
    }
    else {
        // Try to coerce to string and parse
        let strVal = String(val).trim();
        if (isValidDateString(strVal)) {
            millis = new Date(strVal).getTime();
        }

Here is the simplified flow that exhibits the behaviour.

[
    {
        "id": "e75342e810b228f7",
        "type": "debug",
        "z": "fb3b8eb733c0c5e0",
        "name": "Discrete Event",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 860,
        "y": 860,
        "wires": []
    },
    {
        "id": "0683c88ce677ce7a",
        "type": "change",
        "z": "fb3b8eb733c0c5e0",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "sunrise",
                "pt": "msg",
                "to": "$toMillis(payload.sunrise)",
                "tot": "jsonata"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 640,
        "y": 860,
        "wires": [
            [
                "e75342e810b228f7"
            ]
        ]
    },
    {
        "id": "6627d82026f11d88",
        "type": "switch",
        "z": "fb3b8eb733c0c5e0",
        "name": "",
        "property": "payload.sunEvent",
        "propertyType": "msg",
        "rules": [
            {
                "t": "null"
            },
            {
                "t": "else"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 2,
        "x": 410,
        "y": 960,
        "wires": [
            [
                "3a8e0dd0848ee708",
                "f50672bec3e58cfa",
                "0683c88ce677ce7a",
                "e75342e810b228f7"
            ],
            [
                "8ed74b07effbab9b"
            ]
        ]
    },
    {
        "id": "bee2284c3912022b",
        "type": "junction",
        "z": "fb3b8eb733c0c5e0",
        "x": 300,
        "y": 960,
        "wires": [
            [
                "0a11d8da95898da4",
                "6627d82026f11d88"
            ]
        ]
    },
    {
        "id": "792599f715b4c8b7",
        "type": "sun-event-inject",
        "z": "fb3b8eb733c0c5e0",
        "name": "",
        "latitude": "-35.31120",
        "longitude": "149.68053",
        "event": "sunrise",
        "offset": "0",
        "injectEventTimesAfterStartup": true,
        "x": 210,
        "y": 960,
        "wires": [
            [
                "bee2284c3912022b"
            ]
        ]
    },
    {
        "id": "70b8f91ff26d4635",
        "type": "global-config",
        "env": [],
        "modules": {
            "node-red-contrib-sun-event-trigger": "1.1.1"
        }
    }
]

Although I have a workaround that suits me using the function node and converting all date strings to millis when received, it has taken me a while to develop a work around what is I believe is a bug in JSONata implementation... is there something in the node-red-contrib-sun-event-trigger node output I cannot see the cause.

ta

I've just fed the ISO date/time string you provided through a change node set to convert to millis and it worked fine, no errors.

[{"id":"6f334eaa7fed3055","type":"inject","z":"b2f18a716bd20f99","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"2026-05-12T20:48:34.671Z","payloadType":"str","x":190,"y":2020,"wires":[["e727dc4d59e1a0e6"]]},{"id":"fd63f2ac0b883f2b","type":"debug","z":"b2f18a716bd20f99","name":"debug 38","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":620,"y":2020,"wires":[]},{"id":"e727dc4d59e1a0e6","type":"change","z":"b2f18a716bd20f99","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"$toMillis(payload)\t","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":420,"y":2020,"wires":[["fd63f2ac0b883f2b"]]}]

Did I miss something?

Incidentally, I use node-red-contrib-sun-position (node) - Node-RED and have a startup flow that writes sun and moon details to MQTT, and updates the sun and moon positions every minute for good measure.

Ah! Got it! :smiley:

You've unfortunately fallen into one of JavaScripts more confusing traps.

The value you shared is what happens to a JavaScript timestamp when it is serialised to a string - for example, when you dump it to the debug output.

The error you are getting is because your input is not, in fact an ISO timestamp string - but rather an actual JavaScript Date timestamp. $toMillis doesn't actually work with that - presumably because it is unnecessary, you already have the data in millis.

I knew it was not an ISO timestamp string, but did think it was a string that contained a date string that followed the ISO requirements.

generated by the following less the last row (it causes an error as previously described)

now if I change the variable in the last line from nowJSDate to the other variables in created in the node, only sunrise and nowJSDate throw the error, the rest work.

but looking at the debug printout, there is no difference in string format for sunrise, sunriseString, nowJSDate, nowNRDate, or the string inputed to create sunriseJSONataData.

So they are different, but debug does not show the difference between a JSdate, and a string.

Maybe the debug should display JSDate differently from ISO dates, and Strings.

Is there any way to change a JSDate to an ISO date? maybe it should be an extension of the $toMillis() function that if the input is a JSdate (assuming it can recognise that), then it converts it to ISO date (millis).

otherwise we are restricted to using s function node to do the conversion (which I have done).

So many traps when using time/date values, maybe it should be a topic of its own

Yes, but why? You wanted milliseconds and that's what a JS Date is.

This is a JavaScript limitation. new Date() returns a number of milliseconds but within the JavaScript context, const newDate = new Date() is a Date object. Weird but that's what we've got. To display a date object in a web interface, it has to be converted to something and JavaScript Date objects have a built-in toString() function that outputs the ISO timestamp you see in debug (unless you click on the value - debug will rotate through different views for values that look like timestamps). It is not necessarily possible to know if something is a large integer or a Date timestamp.

Indeed. It has always been complex in JavaScript to properly deal with dates and times. However, that is about to be resolved with the new Temporal API. Previously, it has been dealt with using a 3rd-party library.

Node-RED has the moment library built into JSONata for you to use. I also previously created node-red-contrib-moment. You might want to use one of them if you need more ways to process and standardise dates and times.

Woo, holy complexity, Batman

Well, that's JavaScript for you! The Temporal API should be available in node.js v26 I think. There is, I believe, also a polyfill for anyone wanting to use it now: @js-temporal/polyfill

We should also not forget that the INTL API also has various formatting options for dates and times.

Temporal should remove the need for Moment.

Thanks all and I have now cracked it.
The JS date is an object containing a string and a bunch of methods you use in JS to manipulate, but JSONata cannot access the methods, just an object that contains a string which is why $toMillis() does not work (it wants a string not an object).

To convert a JS Date to millis you first need to convert the JSDate object to a string, remove the extra quotes and then convert it to millis.

So, the JSONata statement is

$toMillis(	
   $replace(
	$string(theJSdateObject),
         '"', 
         ""
    )	
)

So thanks all for your help, and JSONata $type() function and a quick copilot search on JS date Objects.

No! As I've already stated, I think more than once, you do not need to do any of this dance at all.

In a function node:

msg.dt = new Date(msg.payload)
msg.ts = Number(msg.dt)
msg.dt2 = msg.dt.toISOString()
return msg

You can pass dt2 to JSONata's $toMillis function. However, the result is IDENTICAL to msg.ts

In other words, and stated again, to output milliseconds from a JavaScript Date object, you simply treat it as a number. The Number() part is not actually necessary since the default value of a Date object IS THE MILLSECONDS. But Monaco objects if you don't force the type to be numeric.

Even easier, the change node and the inject node both have a "timestamp" option which also injects the millis number for now.

Yep, thanks for the Function node Solution, I knew that (but you have a more simple method than I used.
The purpose of publishing the JSONata solution was for those who don't want to work in JS, or want to do a one off in a change node

Sorry, we seem to be not quite joining up here. The point I've made is that there are no cases where anyone needs the complexity you've outlined. Any timestamp being transferred between nodes in Node-RED is either going to be already in Milliseconds or will be an ISO string. Unless someone is dealing with a different string format created elsewhere. The only reason for using the $millis function in JSONata would be in those last cases where the data is already in a string format.