Decision Chaining

There’s an example for decision chaining on github. But it’s designed to work in concert with the bpm engine. I needed it in standalone and did not want to wait for v7.6.0 where it’s added. The following approach works also in today’s standalone engine. Next to the value object, also pass a function object with the label “func” as input parameter. The class looks like this (not optimized yet for resource usage and performance) :

public class CustomFunctions {

public int min(int a, int b) {
    return Math.min(a, b);
}

public String dmn(String dmnId, Map<String, Object> variableContext) {
    System.out.println("evaluate chained DT with id: " + dmnId + ", context: " + variableContext);
    DmnEngine dmnEngine = DmnEngineConfiguration.createDefaultDmnEngineConfiguration().buildEngine();
    InputStream inputStream = CustomFunctions.class.getResourceAsStream("/" + dmnId + ".dmn");
    try {
        DmnDecision decision = dmnEngine.parseDecision(dmnId, inputStream);
        DmnDecisionTableResult result = dmnEngine.evaluateDecisionTable(decision, variableContext);
        System.out.println("sub-result: " + result.getResultList());
        return result.getSingleResult().getSingleEntry();
    } finally {
   ....
    }
}

}

the table “calculateFee” refers to “decideBillingType” in the input definition with this: func.dmn(‘decideBillingType’, variableContext)

It works fine if the chained table only returns one string value. But now the question: If the chained table returns a collection, I tried changing the signature of the method to

public List<  Map< String, Object>> dmn(String dmnId, Map<String, Object> variableContext)

and replace the return with this:

return result.getResultList();

There’s no exception but apparently the list can not be processed as multiple input parameters and I get an empty result instead of two results (caused by the list of input parameters). Does Camunda already support passing something like this [{billing.type=A}, {billing.type=B}] as input (one column in a multi-hit policy table)?

decideBillingType.dmn (2.5 KB)

calculateFees.dmn (2.7 KB)

Hi @neuweiler,

you could use result.collectEntries("billing.type") instead of result.getResultList();. You get a list of the output values and can handle this list in the input expression.

Also, you can have a look at the next 7.6.0 alpha release which will be released next week. This release includes the evaluation of DRDs (i.e. decisions with required decisions). :wink:

Best regards,
Philipp

1 Like

Thanks a lot for the explanation with collectEntries() ! It almost works this way. The result from the nested table is e.g.[A, B] but the calling table returns an empty result (no match for “A” or “B”). Only if I remove the criteria completely, I get a result.

What I see when debugging, in DefaultDmnDecisionContext.evaluateDecisionTable() the content of evaluationResult.inputs is like follows right before calling setEvaluationOutput() : [DmnEvaluatedInputImpl{id='input1', name='Billing Type', inputVariable='cellInput', value=Value '[A, B]' of type 'PrimitiveValueType[string]'}, DmnEvaluatedInputImpl{id='InputClause_0hmkumv', name='Consumption in kWh', inputVariable='cellInput', value=Value '10000' of type 'PrimitiveValueType[integer]'}]

I didn’t dive in any further. But if I can have a look at it in 7.6.0-alpha2, it’s ok. Where would I find an explanation on how to do it ?

Hi @neuweiler,

you can find some example test cases in the DMN engine repository.
Note that we still working on it :wink:

Best regards,
Philipp

Hi @Philipp_Ossler,

A couple of questions:

  1. From what I understood in the example dmn, the elements <informationRequirement><requiredDecision href="#B" /></informationRequirement> refer from e.g. A to B. What I don’t understand is how the result of B will be linked as input to A. Will the informationRequirement elements be mapped to the table’s inputEntry in the same order ?
  2. In the example all decisions are in the same file. Will it be possible to specify external files via an URL like <requiredDecision href="file:/data/dmn/rule.jar!/org/test/RuleB.dmn" /> . If you open an InputStream from the URL “href”, it would be possible to access files inside a jar. Not sure about the #B though… how about adding another attribute to the element: <requiredDecision href="file://data/dmn/rule.jar!/org/test/RuleB.dmn" decisionId="B" />

It would be possible for us to concatenate all chained decisions into one file on the fly… but it’d be one hell of a hack. The approach with external files would be much preferrable. We already load the .dmn files in a way like this from within jar’s and it works fine:

URL url = new URL("file://data/dmn/rule.jar!/org/test/RuleB.dmn");
try (InputStream inputStream = new URL.openStream(url)) {
  DmnDecision decision = dmnEngine.parseDecision(rule.getName(), inputStream);
  return dmnEngine.evaluateDecisionTable(decision, variables).getResultList();
} catch (IOException e) { }

If the solution could do the same with chained decisions, that’d be great.

Cheers,
Michael

Hi Michael,

  1. You can use the output values of a required decision in the input expressions. When a decision is evaluated then the result (i.e. the output values) are stored as variables. This variables can be used in decision which requires the evaluated one. You can find an example in the User Guide.

  2. Currently, the required decision must be in the same DMN file. However, you could use a Decision Literal Expression to evaluate the external decision.

Does this help you?

Best regards,
Philipp

Hi Philipp,

  1. Ah sure… sorry, I could have come to the conclusion myself.
  2. In my current implementation, I already call chained DT’s with literals calling a bean. As the chained decisions are usually intended to be re-used by multiple decisions, it makes more sense to keep them in separate .dmn files. So, it’s either merging them together on-the-fly or using literals. I think I’ll stick with literals for the moment as it’s simpler.

Yes, it helps a lot, thanks! What I need to do now is find a way to get the result of multi-hit tables treated correctly as input for other tables (also tables with multi-ouput). I was postponing this issue as I thought the new implementation would render my approach obsolete.

Can you help me with that? Let’s say, the DT’s in the example of the user guide all have the hit policy “C” and let’s also assume, that “GuestCount” could deliver the result of two rules in one evaluation (e.g. 10 for Holiday and 4 for Weekday). Now the calling DT “Dish” should be evaluated for both results of “GuestCount” and deliver two results in the same evaluateDecision call. Is this possible?
For now, I only got it working, when the bean called by the literal returns result.getFirstResult().getFirstEntry(); . When the bean returns result.collectEntries("guestCount"), then the ArrayList created by collectEntries() becomes a StringTypeImpl later on with the value “[4, 10]” which doesn’t work as input for an Integer input in “Dish”.

Or am I wrong in assuming that the results of multi-hit sub-tables should lead to the execution of the parent decision in all possible variations?

Kind regards,
Michael

Hi Michael,

I try to answer you as good as I can. Maybe you can provide a small example (dmn + test case) and we can solve this by example.

First, if multiple rules match in the required decision “Guest Count” then you have no way to evaluate the decision “Dish” for each matched rule. This case is not yet covered by the DMN spec. As workaround you could use BPMN as coordinator.
The other way is to use the result object in the input expression / input entries of the decision “Dish”. Have a look at the examples: example1 and example2.

Regarding the result type of a decision literal expression: you can specify a result type. If you don’t specify one than the engine should not transform it.

A word to the externalisation of re-usable decisions: Currently, we decided that a required decision must be in the same DMN file. If a decision is used by more than one decision then all decisions must be in one DMN file or the decision must be copied (and renamed! - in case the DMN engine is used in combination with the process engine). This makes it easy if the DMN engine is used in combination with the process engine since it stores all decision definitions inside the repository.
If the required decision can be stored in another DMN file then the process / DMN engine must decide which decision definition (key and version) should be loaded from the repository. This may lead to something like a DMN “call activity”. So it’s not an easy topic.

However, you can create a feature request :wink: If we see that people need this feature then we maybe implement it in further releases.

Best regards,
Philipp

Hi Philipp,

Phew… a difficult topic to tackle via forum. Thanks again for taking your time. I think chaining the decisions with a helper class works fine for us - no need to compile them on-the-fly (hopefully). I attached our test-case in order to explain what we’d expect as proper outcome.

What happens:

  1. We add the parameters consumption (integer) and subscription (“fixed” or “variable”) to the variables.
  2. We instantiate CustomFunctions and add it to the variables under the name func.
  3. We call the DmnEngine to evaluate calculateFees.dmn
  4. Inside calculateFees we call func.evaluate("rules.decideBillingType", variableContext). This calls the method CustomFunctions.evaluate() which loads and executes the rule decideBillingType. The result of which is used as input in calculateFees.
  5. calculateFees returns the tuple of billingAmount and billingType.

For any combination where consumption is <2500, we expect only one result from decideBillingType (either minimum, A or B). This works fine in the entire chain. But if we set the subscription = “variable” and the consumption to 3000, the rules 3 and 4 of decideBillingType fire and we get ["A", "B"] as result. The expected behaviour of calculateFees would be that with this input, the rules 2 and 3 would fire and thus return two results: [{billingAmount=1250,billingType="A"},{billingAmount=100,billingType="B"}]

Is this really not part of DMN1.1? Then it surely must be included in DMN 1.2 ! :slight_smile: This is critical for daily business use. I’d call it “Chaining of Multi-Hit DT’s”.

calculateFees.dmn (2.8 KB)
decideBillingType.dmn (2.6 KB)
CustomFunctions.java.txt (1.8 KB) (sorry for the .txt but the forum doesn’t allow .java to be uploaded)

Hi Michael,

I think one way to reach your goal is to change the expression in the input entry “Billing Type” in “Calculate Fees” decision so that it work on a list instead of a single value. Assuming the variable “Billing Type” is always a list then the expression could be func.contains(cellInput, "A"). The variable cellInput holds the “Billing Type” (e.g. [“A”, “B”]) - see input variable name for details.

Does this help you to solve your issue?

I don’t see that DMN 1.1 take care of the chaining behavior. You can also have a look at the specs and provide your inputs if you find something :slight_smile:
Since DMN 1.2 is not released yet, I’m not sure what will be part of it. But I heared that they discuss something about multi-instance decision evaluation.

BTW, you can use the DMN unit test template on GitHub to provide code or a failing test case.

Best regards,
Philipp

Hi Philipp,

That’s probably the way to go! But I couldn’t get it running yet - even with all possible permutations I could come up with. When using func.contains(cellInput, "A") then surprisingly a method with the signature contains(Object) is called, instead of contains(Object, Object) - although we try to pass two arguments. (Or I get an exception Caused by: camundafeel.javax.el.MethodNotFoundException: Cannot find method contains with 1 parameters in class com.innoq.brms.client.CustomFunctions if the method contains(Object) is missing). Even if I use func.contains("A", "B", "C", "D") it only calls contains(Object) with the first argument “A”.

Then I tried an approach with cellInput.contains("A") but then we get into trouble that the input typeRef is specified as String and this call returns a Boolean. Now if I change the input typeRef to Boolean, the eval logic gets into trouble with the returned [“A”,“B”]: org.camunda.bpm.dmn.engine.DmnEngineException: DMN-01005 Invalid value 'A' for clause with type 'boolean'. - ein Teufelskreis

The second approach would be the nicer one… but then even with input typeRef == Boolean, it would have to allow List of Strings or List of Maps as input. So it could be evaluated as boolean expression.

I’m sorry, but I’m at a loss here. The only way I’d see now that the enginge’s code would be altered so if cellInput is of type List, it would execute each rule for each value in the list. Or that it would accept a List of Objects as input if the typeRef is Boolean.

DMN Spec: Well, I couldn’t read the entire spec but there are some indications :

  • 8.2.4: “The list of input values is optional. If provided, it is a list of unary tests that must be satisfied by the corresponding input.
  • 5.3.3: “Input decisions: instances of the results of all the input decisions.

but both may be completely out of context…

Maybe we’ve reached the limits of DMN here and the logic should be implemented in another language (MVEL, Java, …).

Hi Michael,

can you please provide a failing unit test using the template on GitHub? Otherwise, I need more time to build it myself. I think that we will find a way to get it working.

Regarding the DMN spec, both chapters are out of context :wink:

Best regards,
Philipp

I will… (tomorrow). In the meantime I think I’ve found a limited workaround: If I use the following in the input field of the rule, I can check for the various combinations:
“[{billingType=A}, {billingType=B}]”
“[{billingType=A}]”
“[{billingType=B}]”

But it’ll require more rule entries in a DT and only works if the chained DT can only return a small amount of values.

DMN Spec: I had a feeling it was :slight_smile:

Thanks for your great support !!!

In DMN 1.1 the suggested way to handle collection input data is a boxed expression which evaluates the collection:

In Camunda the equivalent of the context would be a JUEL expression in an input expression which references the collection input as in FooCollection.contains("BAR"). With feel-scala one can even use the list contains FEEL function.

Obviously you have to write many such evaluations in order to test for the presence of many values in the collection.

It seems DMN 1.2 solves this with the new Feature: More Powerful Test Expressions in Decision Tables, described in A Practitioner’s Review of DMN 1.2

1 Like

Hi all, i am pretty new to camunda i was migrating a activity project to camunda and in a DMN file i found an empty operator since activity uses MVEL i am quite confused what is the equivalent operator in FEEL camunda ?

Hi @Sebin_Johnson,

there is no direct support for null in the Camunda DMN engine. However, you could use another expression language like JUEL for the check. Or, model the decision in a way that you don’t need the null check.

Have a look at the related topic: Processing of nulls in dmn tables

Does this help you?

Best regards,
Philipp

Thank you @Philipp_Ossler i was able to do this using a “” to check if the string i got is empty i actually didn’t need to get the same functionality as that of empty operator