Trigger, on start (and repeat every hour).
Change node with some fancy JSONata to calculate the delay in milliseconds to the next hour
Delay node, set to accept msg.delay
Seems to work fine for me. Been firing every hour on the hour just as requested.
[{"id":"9a6b636812953f70","type":"inject","z":"987b2e9d10a9bd13","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"3600","crontab":"","once":true,"onceDelay":0.1,"topic":"","payloadType":"date","x":730,"y":3740,"wires":[["a72a41bd8f7ce6c3"]]},{"id":"83f913da7e4ec2f5","type":"delay","z":"987b2e9d10a9bd13","name":"","pauseType":"delayv","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":1060,"y":3740,"wires":[["38acfdb525541374"]]},{"id":"38acfdb525541374","type":"debug","z":"987b2e9d10a9bd13","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1210,"y":3740,"wires":[]},{"id":"a72a41bd8f7ce6c3","type":"change","z":"987b2e9d10a9bd13","name":"","rules":[{"t":"set","p":"delay","pt":"msg","to":"(\t $time := ($now('[H]:[m]:[s]')~>$split(':')).$number();\t 1000*(60*(60-$time[1]) - $time[2])\t)\t","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":900,"y":3740,"wires":[["83f913da7e4ec2f5"]]}]