Passing variables to rules in OH 5.2.1

593 / 5.000

Hello,

I’m currently trying to migrate from OH 5.1.3 to OH 5.2.1.

Unfortunately, it’s no longer possible to pass an argument to a called rule in JavaScript. For example, in the calling rule:

rules.runRule(‘TestRule1’, {GlobalStatic});

Accessing the variable GlobalStatic is then not possible in TestRule1 in version 5.2.1.

Since my entire software is structured so that a data structure is passed to all called rules, I have the following question:

Does anyone have an idea how to enable this access?

Please provide a snippet from your actual code where one can see variable declarations as well.

/cc @rlkoshak

Hi, here the code from calling rule:

var GlobalStatic = cache.private.get("GlobalStatic");

if ((typeof GlobalStatic === 'undefined') || (GlobalStatic === null)){
			GlobalStatic = {};
}

GlobalStatic.TestValue = "Test"
console.log(GlobalStatic.TestValue);

rules.runRule('TestRule1', {GlobalStatic});

cache.private.put("GlobalStatic", GlobalStatic);

And here there code from the called rule:

console.log(GlobalStatic.TestValue);

GlobalStatic is red marked…

Marked in red is just caused by the fact that the code highlighter is more strict now. As far as the highlighter is concerned, GlobalStatic doesn’t exist in that script.

You can make the red highlight go away by using console.log(ctx.GlobalStatic.TestValue);.

But that doesn’t tell us what actual error you see when the rules run.

Note, this is an unusual configuration. If this data is as it’s named (i.e. static) a more appropriate approach would be to make this a library imported by all the scripts that use it. If it’s misnamed and not actually static, beware that the following might cause problems.

  1. To solve the problem where two rules call the same rule at the same time the called rule now runs in its own thread.
  2. GraalVM JS does not allow access to JS data from multiple threads at the same time.
  3. GraalVM does allow access to Java data from multiple threads at the same time.
  4. 5.2.1 adds a javafi and jsify method when passing values from one rule to another or adding stuff to the shared cache. This converts JS data to Java (javaify) and converts Java to JS (jsify). However, functions do not survive this transformation. While it should not be an issue, there migth be an edge case here.

So what is in GlobalStatic? What exceptions are you seeing in the logs? Or is it just the syntax highlighting?

Hi Rich,

Thanks so much for your reply.

Surprisingly, it works in the test environment despite the red marker. There are no error lines at all. However, it doesn’t work in the real environment; substructures of GlobalStatic are being continuously reinitialized.

There are still no error lines, but the data arrives as “undefined” on the next call.

I’m starting to think the real structure is too extensive…

I’ll investigate the real environment separately, but that might take some time.

I use this “GlobalStatic” variable in all rules as static storage with their respective substructures, and of course, for data exchange between the rules.

Here’s a small excerpt (roller shutter control, about 10%) of the currently used structure.

console.log(GlobalStatic.Rules.PowerJalusien_V11); →

2026-08-12 16:46:54.814 [INFO ] [on.jsscripting.rule.Crontab1s_NewV11] -{
“TimerName”: “PJ_HeatProtectionOff”,
“TimerInput”: false,
“TimerOutputHeat1”: false,
“TimerOutputHeat2”: false,
“HeatProtection”: true,
“GarageBedUP”: true,
“RollerObjGarageS”: {
“NRSollPos”: 10,
“IsBlocked”: false,
“RoomName”: “GarageS”,
“RollerItem”: “RS_126”,
“WinOpen”: false,
“WindWarning”: false,
“TimerName”: “PJ_GarageS”,
“LowLimit1”: 10,
“LowLimit2”: 10,
“WinDelay”: 15,
“DownLimit”: 10,
“UpLimit”: 100,
“Automatik”: true,
“SommerPos”: 50,
“SommerPosAktiv”: false,
“TimerOutput”: false,
“TimerOutput1”: true,
“TimerOutput2”: false,
“StorageTaster”: false,
“IstPos”: 10,
“Button1”: false,
“ButtonPos1”: 100,
“Button2”: false,
“ButtonPos2”: 0,
“Rauchwarnmeldung”: false,
“WinDelayStorage”: 15,
“ActualDestination”: 10,
“DebugModus”: false
},
“RollerObjGarageG”: {
“NRSollPos”: 20,
“IsBlocked”: false,
“RoomName”: “GarageG”,
“RollerItem”: “RS_127”,
“WinOpen”: false,
“WindWarning”: false,
“TimerName”: “PJ_GarageG”,
“LowLimit1”: 20,
“LowLimit2”: 20,
“WinDelay”: 15,
“DownLimit”: 20,
“UpLimit”: 100,
“Automatik”: true,
“SommerPos”: 50,
“SommerPosAktiv”: false,
“TimerOutput”: false,
“TimerOutput1”: true,
“TimerOutput2”: false,
“StorageTaster”: false,
“IstPos”: 20,
“Button1”: false,
“ButtonPos1”: 100,
“Button2”: false,
“ButtonPos2”: 0,
“Rauchwarnmeldung”: false,
“WinDelayStorage”: 15,
“ActualDestination”: 20,
“DebugModus”: false
},
“RollerObjLivingT”: {
“NRSollPos”: 100,
“IsBlocked”: true,
“RoomName”: “LivingT”,
“RollerItem”: “RS_131”,
“WinOpen”: false,
“WindWarning”: false,
“TimerName”: “PJ_LivingT”,
“LowLimit1”: 40,
“LowLimit2”: 35,
“WinDelay”: 30,
“DownLimit”: 0,
“UpLimit”: 100,
“Automatik”: true,
“SommerPos”: 100,
“SommerPosAktiv”: false,
“TimerOutput”: false,
“TimerOutput1”: true,
“TimerOutput2”: false,
“StorageTaster”: false,
“IstPos”: 100,
“Button1”: false,
“ButtonPos1”: 100,
“Button2”: false,
“ButtonPos2”: 0,
“Rauchwarnmeldung”: false,
“WinDelayStorage”: 30,
“ActualDestination”: 100,
“DebugModus”: false
},
“RollerObjLivingS”: {
“NRSollPos”: 100,
“IsBlocked”: false,
“RoomName”: “LivingS”,
“RollerItem”: “RS_128”,
“WinOpen”: false,
“WindWarning”: false,
“TimerName”: “PJ_LivingS”,
“LowLimit1”: 100,
“LowLimit2”: 100,
“WinDelay”: 15,
“DownLimit”: 0,
“UpLimit”: 100,
“Automatik”: true,
“SommerPos”: 70,
“SommerPosAktiv”: false,
“TimerOutput”: false,
“TimerOutput1”: true,
“TimerOutput2”: false,
“StorageTaster”: false,
“IstPos”: 100,
“Button1”: false,
“ButtonPos1”: 100,
“Button2”: false,
“ButtonPos2”: 0,
“Rauchwarnmeldung”: false,
“WinDelayStorage”: 15,
“ActualDestination”: 100,
“DebugModus”: false
},
“RollerObjKitchen”: {
“NRSollPos”: 100,
“IsBlocked”: false,
“RoomName”: “Kitchen”,
“RollerItem”: “RS_129”,
“WinOpen”: true,
“WindWarning”: false,
“TimerName”: “PJ_Kitchen”,
“LowLimit1”: 75,
“LowLimit2”: 15,
“WinDelay”: 240,
“DownLimit”: 0,
“UpLimit”: 100,
“Automatik”: true,
“SommerPos”: 50,
“SommerPosAktiv”: false,
“TimerOutput”: false,
“TimerOutput1”: true,
“TimerOutput2”: false,
“StorageTaster”: false,
“IstPos”: 100,
“Button1”: false,
“ButtonPos1”: 100,
“Button2”: false,
“ButtonPos2”: 100,
“Rauchwarnmeldung”: false,
“WinDelayStorage”: 240,
“ActualDestination”: 100,
“DebugModus”: false
},
“RollerObjToilet”: {
“NRSollPos”: 100,
“IsBlocked”: false,
“RoomName”: “Toilet”,
“RollerItem”: “RS_136”,
“WinOpen”: false,
“WindWarning”: false,
“TimerName”: “PJ_Toilet”,
“LowLimit1”: 50,
“LowLimit2”: 10,
“WinDelay”: 60,
“DownLimit”: 0,
“UpLimit”: 100,
“Automatik”: true,
“SommerPos”: 50,
“SommerPosAktiv”: false,
“TimerOutput”: false,
“TimerOutput1”: true,
“TimerOutput2”: false,
“StorageTaster”: false,
“IstPos”: 100,
“Button1”: false,
“ButtonPos1”: 100,
“Button2”: false,
“ButtonPos2”: 100,
“Rauchwarnmeldung”: false,
“WinDelayStorage”: 60,
“ActualDestination”: 100,
“DebugModus”: false
},
“RollerObjBedroom”: {
“NRSollPos”: 20,
“IsBlocked”: false,
“RoomName”: “Bedroom”,
“RollerItem”: “RS_132”,
“WinOpen”: true,
“WindWarning”: false,
“TimerName”: “PJ_Bedroom”,
“LowLimit1”: 50,
“LowLimit2”: 6,
“WinDelay”: 5,
“DownLimit”: 0,
“UpLimit”: 100,
“Automatik”: true,
“SommerPos”: 20,
“SommerPosAktiv”: true,
“TimerOutput”: false,
“TimerOutput1”: true,
“TimerOutput2”: false,
“StorageTaster”: false,
“IstPos”: 20,
“Button1”: false,
“ButtonPos1”: 100,
“Button2”: false,
“ButtonPos2”: 0,
“Rauchwarnmeldung”: false,
“WinDelayStorage”: 5,
“ActualDestination”: 20,
“DebugModus”: false
},
“RollerObjChild1”: {
“NRSollPos”: 15,
“IsBlocked”: false,
“RoomName”: “Child1”,
“RollerItem”: “RS_133”,
“WinOpen”: true,
“WindWarning”: false,
“TimerName”: “PJ_Child1”,
“LowLimit1”: 25,
“LowLimit2”: 15,
“WinDelay”: 45,
“DownLimit”: 0,
“UpLimit”: 100,
“Automatik”: true,
“SommerPos”: 25,
“SommerPosAktiv”: true,
“TimerOutput”: true,
“TimerOutput1”: true,
“TimerOutput2”: false,
“StorageTaster”: false,
“IstPos”: 15,
“Button1”: false,
“ButtonPos1”: 100,
“Button2”: false,
“ButtonPos2”: 100,
“Rauchwarnmeldung”: false,
“WinDelayStorage”: 45,
“ActualDestination”: 15,
“DebugModus”: false,
“AbsencePos”: 15,
“AbsenceActiv”: false
},
“RollerObjChild2_L”: {
“NRSollPos”: 30,
“IsBlocked”: false,
“RoomName”: “Child2_L”,
“RollerItem”: “RS_134”,
“WinOpen”: false,
“WindWarning”: false,
“TimerName”: “PJ_Child2_L”,
“LowLimit1”: 40,
“LowLimit2”: 20,
“WinDelay”: 5,
“DownLimit”: 0,
“UpLimit”: 100,
“Automatik”: true,
“SommerPos”: 30,
“SommerPosAktiv”: true,
“TimerOutput”: false,
“TimerOutput1”: true,
“TimerOutput2”: false,
“StorageTaster”: false,
“IstPos”: 30,
“Button1”: false,
“ButtonPos1”: 100,
“Button2”: false,
“ButtonPos2”: 100,
“Rauchwarnmeldung”: false,
“WinDelayStorage”: 5,
“ActualDestination”: 30,
“DebugModus”: false
},
“RollerObjChild2_R”: {
“NRSollPos”: 30,
“IsBlocked”: false,
“RoomName”: “Child2_R”,
“RollerItem”: “RS_135”,
“WinOpen”: true,
“WindWarning”: false,
“TimerName”: “PJ_Child2_R”,
“LowLimit1”: 40,
“LowLimit2”: 20,
“WinDelay”: 5,
“DownLimit”: 0,
“UpLimit”: 100,
“Automatik”: true,
“SommerPos”: 30,
“SommerPosAktiv”: true,
“TimerOutput”: false,
“TimerOutput1”: true,
“TimerOutput2”: false,
“StorageTaster”: false,
“IstPos”: 30,
“Button1”: false,
“ButtonPos1”: 100,
“Button2”: false,
“ButtonPos2”: 100,
“Rauchwarnmeldung”: false,
“WinDelayStorage”: 5,
“ActualDestination”: 30,
“DebugModus”: false
}
}

So the Object is misnamed. It’s not static.

Use code fences when posting JSON.

```
code goes here
```

So is it’s basically a map of maps using JS primitives as the values. These should survive the javaify and jsify conversions just fine. However, you say this is only a portion of the data. If there are more complex JS Objects in there those will not survive the translation.

If I read this map correctly, most of the data is keyed on Item names. Have you considered using Item metadata instead? All the values seem to be appropriate for Item metadata. Of course, Item metadata can be accessed and modified from rules.

Some of the data seems redundant assuming you are using the semantic model (e.g. RoomName could be acquired using items.MyItem.semantics.location).

This is a simple way for me to work with variables that retain their value from call to call, but they don’t retain their value after a system restart.

Many substructures have grown organically over the years, and with the update to OH 5.1.3, all substructures were consolidated. There’s no redundancy; all values ​​are processed and evaluated separately.

In OH 5.1.3, everything works flawlessly and integrates perfectly. Switching to metadata would ultimately mean rewriting almost the entire code.

I’ve now created an example where it doesn’t work in OH 5.2.1; the structure is corrupted in the called rule. Inside SubRule no error message appears.

See here, MainRule:

var GlobalStatic = cache.private.get("GlobalStatic");

if ((typeof GlobalStatic === 'undefined') || (GlobalStatic === null)){
  GlobalStatic = {};
  console.log("GlobalStatic is initialized")
}

if ((typeof GlobalStatic.Rules === 'undefined') || (GlobalStatic.Rules === null)){
  GlobalStatic.Rules = {};
  console.log("GlobalStatic.Rules is initialized")
}

rules.runRule('SubRule', {GlobalStatic});

console.log("Log of TestVar: " + GlobalStatic.Rules.SubRule.TestVar);

cache.private.put("GlobalStatic", GlobalStatic);

SubRule:


if ((typeof ctx.GlobalStatic.Rules.SubRule === 'undefined') || (ctx.GlobalStatic.Rules.SubRule === null)){
  ctx.GlobalStatic.Rules.SubRule = {};
  console.log("GlobalStatic.Rules.SubRule is initialized")
}



ctx.GlobalStatic.Rules.SubRule.TestVar = "TestValue";

console.log("Value TestVar: " + ctx.GlobalStatic.Rules.SubRule.TestVar);

Log:

2026-08-13 08:50:29.931 [INFO ] [automation.jsscripting.rule.MainRule] - GlobalStatic is initialized
2026-08-13 08:50:29.934 [INFO ] [automation.jsscripting.rule.MainRule] - GlobalStatic.Rules is initialized
2026-08-13 08:50:29.937 [INFO ] [.automation.jsscripting.rule.SubRule] - GlobalStatic.Rules.SubRule is initialized
2026-08-13 08:50:29.938 [INFO ] [.automation.jsscripting.rule.SubRule] - Value TestVar: TestValue
2026-08-13 08:50:29.940 [ERROR] [.handler.AbstractScriptModuleHandler] - Script execution of rule with UID 'MainRule' failed: TypeError: Cannot read property "TestVar" from undefined in <eval> at line number 17 at column number 34
2026-08-13 08:50:31.708 [INFO ] [automation.jsscripting.rule.MainRule] - GlobalStatic is initialized
2026-08-13 08:50:31.713 [INFO ] [automation.jsscripting.rule.MainRule] - GlobalStatic.Rules is initialized
2026-08-13 08:50:31.722 [INFO ] [.automation.jsscripting.rule.SubRule] - GlobalStatic.Rules.SubRule is initialized
2026-08-13 08:50:31.727 [INFO ] [.automation.jsscripting.rule.SubRule] - Value TestVar: TestValue
2026-08-13 08:50:31.730 [ERROR] [.handler.AbstractScriptModuleHandler] - Script execution of rule with UID 'MainRule' failed: TypeError: Cannot read property "TestVar" from undefined in <eval> at line number 17 at column number 34

The question is: What was changed in version 5.2.1 that causes this behavior?

openHAB 5.2.0 fixed a long standing flaw in the execution logic of rules.runRule, making the rule execute in its own thread instead of the thread of the calling rule.
This change however means that you can no longer pass JS objects between rules as this would lead to multi-threaded access to them which GraalJS does not allow (it throws an ISE in that case).
What is technically allowed however is passing Java objects. Using the passed-in context as a two-way data binding is bad practise IMO, as there are no concurrency guards and modifying data from different threads may lead to unexpected behaviour.

When a JS object is passed to a different rule, the library internally Java-ifies it. This way, a copy of the data is passed as Java types, which is both technically allowed and also free from concurrency issues.

To allow for a two-way data binding, manually javaify (using utils.javaify) your GlobalStatic before passing it and use the java-ified version in your main rule. Another option would be to pass the copy and return the modified version from the other rule.
Or use the shared cache instead.

Not really. It just changes those few lines that add or pull data from the GlobalStatic. And those lines would change, not be replaced. Nothing else about your rules would need to change as the way Metadata stores data is basically the same as your GlobalStatic does.

@scuba, some more alternatives include:

  • if it’s static data, put it in a library and require it
  • use Item metadata
  • use Items
  • use Java for the GlobalStatic data structure from the start; note this still has a risk if the data changing unexpectedly changing as only the access to the HashMap is synchronized, another rule could change the data after you access it
  • break the data passed to the called rule into separate arguments and only pass primitives instead of a big complicated data structure.
  • break the data up into the shared cache instead of a single variable. The shared cache is a Map so it behaves the similarly to your Global Static Object only with different syntax because it’s a Map. That means you can use the shared cache almost as a line-for-line replacement for GlobalStatic and in some ways save some lines.
// If 'Rules' doesn't already exist it's initialized as a new Map
// This JS Map will be converted to a Java Map but it should behave the same 
// Note, as written this line is optional. The called rule will initialize the 'Rules' too if it doesn't exist
cons rules = cache.shared.get('Rules', () => new Map()); 
rules.runRule('SubRule'); // if needed you can pass th key to pull from the shared cache
console.log("Log of TestVar: " + cache.shared.get('Rules').get('TestVar');
cache.shared.get('Rules', () => new Map()).set('TestVar', 'TestValue');
console.log("Value TestVar: " + cache.shared.get('Rules').get('TestVar'));

Things of note:

  • these are Maps, not Objects so you have to use set and put
  • the cache.stared will always be there, you don’t really need to test for it
  • the anonymous function passed to get is an initialization function; the that key is empty it calls the function and puts the value into the cache and then returns it

Unfortunately, the change to 5.2 was required. Far too many users were seeing inexplicable multi-threaded exceptions because they had two or more rules calling the same rule at the same time. But because of limitations in GraalVM JS, the fix means we cannot pass JS stuff to the called rule and we cannot put JS stuff into the shared cache. Therefore auto conversions were added to both.

Also, when you pass data to a called rule, you are passing a copy of that data. No longer are you passing a reference. This means changes made in the called rule will not be reflected in the calling rule. To achieve that you must use the shared cache.

I also think that the called rule can return something to the calling rule now. I haven’t experimented with this yet though so I’m not 100% certain how it works. The docs mention “return values” which implies it’s not just a simple return someValue. But that is another option for letting rules communicate.

I think Graal allows to access map properties using .key syntax.

I tired that first in the scratchpad and it didn’t work. I may have done something wrong or the javaification/jsification got in the way perhaps.

I tried various combinations of

cache.shared.get('Rules', () => new Map()).TestVar = 'TestValue';
console.log("Value TestVar: " + cache.shared.get('Rules').TestVar);

It always threw errors. I could only get it to work using set and get.

Hello,

first of all, thank you so much for your replies!

I’ve now extracted everything that’s only needed locally from GlobalStatic, where it was only used for local instantiation. See here:

  // Starten der 1s Rules
  rules.runRule('Button_V11', {GlobalStatic});
  rules.runRule('PowerLights_V11', {GlobalStatic});
  rules.runRule('DecreaseTimer', {});
  rules.runRule('PowerDetection', {GlobalStatic});
  rules.runRule('PowerFast_V11', {GlobalStatic});
  rules.runRule('PowerJalusien_V11', {GlobalStatic});  
  rules.runRule('TriggerWinDoorsRule_V11', {});
  

 

  // Starten der 5s Rules
  if ((LocalStatic.CallCounter % 5) == 0){  
    rules.runRule('Miscellaneous_V11', {GlobalStatic});
    rules.runRule('Anwesenheit', {GlobalStatic});
    rules.runRule('TempSensors_V11', {GlobalStatic});  
    rules.runRule('PowerSum_V11', {GlobalStatic});

    
    rules.runRule('NoonHeatRule_V11', {});
    rules.runRule('PowerSlow_V11', {GlobalStatic});    
    rules.runRule('ViessmannReadValues_V11', {});
    rules.runRule('AC_Wohnen_V11', {GlobalStatic});
    rules.runRule('HeatLivingroomRule_V11', {GlobalStatic});
    rules.runRule('HeatChild2Rule_V11', {GlobalStatic});
    rules.runRule('HeatBedroomRule_V11', {GlobalStatic});
    rules.runRule('HeatChild1Rule_V11', {GlobalStatic});
    rules.runRule('HeatBathroomRule_V11', {GlobalStatic});
    rules.runRule('HeatKitchenRule_V11', {GlobalStatic});
    rules.runRule('ViessmannWriteValues_V11', {GlobalStatic});

   
    if (DebugModus) {console.log(Name + " Beende Ausführung 5s...");}
  }


  // Starten der 10s Rules
  if ((LocalStatic.CallCounter % 10) == 0){
    rules.runRule('PushSaferRule1_V11', {GlobalStatic});
    rules.runRule('PushSaferRule2_V11', {GlobalStatic});
    rules.runRule('PushSaferRule3_V11', {GlobalStatic});
    // rules.runRule('Boenen_V11', {});
    rules.runRule('CalcWeather_V11', {GlobalStatic});
    rules.runRule('MonthlyReset_V11', {GlobalStatic});
    rules.runRule('ProcessEnergy_V11', {GlobalStatic});
    rules.runRule('BlockyTest1', {GlobalStatic});

    
    if (DebugModus) {console.log(Name + " Beende Ausführung 10s...");}
  }

  
  // Starten der 1h Rules
  if ((LocalStatic.CallCounter % 3600) == 0){
    rules.runRule('Emphermeris', {GlobalStatic});
    if (DebugModus) {console.log(Name + " Beende Ausführung 1h...");}
  }

Unfortunately, this only resolved the problem for 3 out of 29 rules; the other 26 rules exchange data. These are exclusively one-to-many relationships: one writer, multiple readers.

Using “var GlobalStatic = cache.shared.get(“GlobalStatic”);” would probably mean re-reading the data at every single location where it’s used…

“return someValue” sounds very good; it was an option I considered during the initial development. However, multi-threading also leads to asynchronous programming, and it’s rather unlikely that the called rule will have finished by the time the caller jumps to the next line after the call…

Will it be possible to switch between single-threaded and multi-threaded modes in future versions via a setting?

That’s not how JS Scripting works. There is a hard limit with GraalVM JS that only one thread can access the same JS Objects at the same time. All that had been done is to ensure that all JS rules run single threaded. There is and never had been the ability to run multithreaded.

But it is true that each and every rule runs independently in it’s own thread. This means if there is any shared JS data between rules, those two rules that share the data cannot run at the same time.

Before 5.2 1 it was possible for two separate roles to call the same rule at the same time, causing a Multithreaded exception since Multithreaded execution is not allowed.

In 5.2.1 to help solve this problem and others, the called rule no longer executes in the calling rule’s thread. Now the called rule runs in its own thread.

As a consequence, you can no longer pass JS Objects when you call another rule. This is a hard limit. Before it was buggy and the only way to make it not buggy is to not allow it.

Hence the one thing in common with every recommendation above is that it makes it so the shared data isn’t a JS Object.

But this is never going to be configurable. It’s a hard limitation of GraalVM JS that all access to JS Objects must be single threaded. The changes made to 5.2.1 ensures this in the only place left where this single threaded access wasn’t enforced in OH already.

As for your concern about using return values from the called rules, you need not worry. In OH 5.1 and before the calling rule has always blocked until the called rule completes. It couldn’t run the next line until the called rule exits.

In 5.2.1+ you now have two options to run a rule, synchronous (the default) or asynchronously. But the only way to receive the return values is to run the rules synchronously. The calling rule will block until the called rule returns. It can’t execute the next line until the called rule returns.

That sounds great, and it gives the user all the options they need!

I’ll wait until version 5.2.1+ is released and then continue working in my familiar environment.