Rules SDK - Writing Rules - Overview
Author:
Fluent Commerce
Changed on:
30 June 2024
Author:
Fluent Commerce
Changed on:
30 June 2024
Author:
Fluent Commerce
Changed on:
5 Sept 2025
`SendEventAction``MutateAction``WebhookAction``LogAction``SendEventAction` provides the mechanism for sending new events.A common use case for this is to control flow, and trigger a new Ruleset for execution after completing the current one. These types of events are typically queued and executed within the same execution thread, as long as they are for the same context.Another common scenario for this action is to trigger a Ruleset in a different Workflow. These events cannot be executed on the same execution thread, and should be sent out of Rubix, to be processed on a separate execution context.`SendEventAction`1public class MyCustomRule {
2
3 //...
4
5 public void run(C context) {
6
7 //...
8
9 Event currentEvent = context.getEvent();
10 Event flowControlEvent = currentEvent.toBuilder().name("MyNextRuleset").build();
11 context.action().sendEvent(flowControlEvent);
12 }
13} 1public class MyCustomRule {
2
3 //...
4
5 public void run(C context) {
6
7 //...
8
9 Event newWorkflowEvent = Event.builder()
10 .rootEntityRef("IC_123")
11 .rootEntityType("INVENTORY_CATALOGUE")
12 .entityRef("IP_321")
13 .entityType("INVENTORY_POSITION")
14 .entitySubtype("DEFAULT")
15 .scheduledOn(new Date())
16 .retailerId(context.getEvent().getRetailerId())
17 .source(context.getEvent().getId().toString())
18 .name("RulesetName")
19 .attributes(myEventAttributes)
20 .build();
21
22 context.action().sendEvent(newWorkflowEvent);
23 }
24}1public class MyCustomRule {
2
3 //...
4
5 public void run(C context) {
6
7 //...
8
9 Event currentEvent = context.getEvent();
10
11 Event newRetailerEvent = currentEvent.builder()
12 .retailerId(context.getEvent().getRetailerId())
13 .source(context.getEvent().getId().toString())
14 .name("RulesetName")
15 .attributes(myEventAttributes)
16 .build();
17
18 context.action().sendEvent(newWorkflowEvent);
19 }
20}1public class MyCustomRule {
2
3 //...
4
5 public void run(C context) {
6
7 //...
8
9 Event currentEvent = context.getEvent();
10
11 Date anHourFromNow = DateUtils.addHours(new Date(), 1);
12
13 Event newRetailerEvent = currentEvent.builder()
14 .name("RulesetName")
15 .scheduledOn(anHourFromNow)
16 .source(context.getEvent().getId().toString())
17 .build();
18
19 context.action().sendEvent(newWorkflowEvent);
20 }
21}`MutateAction` provides the mechanism for calling GraphQL Mutations as per the Fluent GraphQL Schema. Mutations provide a way to create or update data within the platform.`MutateAction``MutateAction` from a Rule to execute against the GraphQL API.First, you will need to construct a Mutation object. You should add your mutation query GraphQL file to the `graphql` folder in your plugin project, and perform a Maven build to generate the relevant mutation object.Once you have the Mutation object generated for use within your Rule, you simply need to build it with the relevant data, and pass it as a parameter to the Mutation Action:1public class MyCustomRule {
2
3 //...
4
5 public void run(C context) {
6
7 //...
8
9 UpdateOrderInput updateOrderInput = UpdateOrderInput.builder()
10 .id(context.getEntity().getId())
11 .attributes(attributeInputList)
12 .build();
13
14 UpdateOrderAttributesMutation updateOrderAttributesMutation = UpdateOrderAttributesMutation.builder()
15 .input(updateOrderInput)
16 .build();
17
18 context.action().mutation(updateOrderAttributesMutation);
19 }
20}`WebhookAction` provides an integration ability, whereby data can be sent to an external endpoint.Read more on Webhooks, and how to build the Webhook receiver in the Integration section.`WebhookAction`1public class MyCustomRule {
2
3 //...
4
5 public void run(C context) {
6
7 //...
8
9 context.action().postWebhook(url, context.getEvent());
10 }
11}`LogAction` provides the ability to add a custom Orchestration Audit Event.The additional orchestration audit event will be associated to the same context as the parent event, making it a useful approach to adding additional information about the current execution.`LogAction`1public class MyCustomRule {
2
3 //...
4
5 public void run(C context) {
6
7 //...
8
9 context.action().log(message, detailedMessage, attributes);
10 }
11}Author:
Fluent Commerce
Changed on:
30 June 2024
`run` method is called by the Workflow Engine when executing each Rule in a Ruleset triggered by an Orchestration Event.`run` method receives a `Context` instance, which provides all the necessary contextual inputs to the Rule, as well as access to the Fluent API Client, and the Workflow Engine ActionFactory.Rules are singletons, meaning the same single instance of the Rule is processing multiple threads. To this point, make sure you do not declare any runtime-specific values in the Rule Class properties, as these will not be threadsafe.To implement the Rule, the following steps are usually followed within the `run` method:`context.action()` ActionFactory to produce the Action`RuleUtils`For example, this snippet validates that the Order Id parameter exists:1// imports & Rule Info annotation...
2@ParamInteger(name = "MyInt", description = "An Integer value for the Rule")
3public class MyCustomRule implements Rule {
4
5 // local fields...
6
7 public void run(C context) {
8
9 // Validation:
10 RuleUtils.validateRuleProps(context, "MyInt");
11
12 // continuing logic...
13
14 }
15}`return` statement to immediately exist the Rule but continue processing the Ruleset.`RuleUtils` has a mix of both boolean response and thrown exceptions.`Context` does already contain an Entity (`context.getEntity()`), however, this is a subset of the Entity itself.It only contains the primary information (the common generic fields) of an Orchestrateable Entity:`Context`.For example, if you are writing a rule that needs to operate on a field or attribute of the Event Entity, you can retrieve this via a GraphQL query.1// imports & Rule Info annotation...
2@ParamInteger(name = "MyInt", description = "An Integer value for the Rule")
3public class MyCustomRule implements Rule {
4
5 // local fields...
6
7 public void run(C context) {
8
9 // ...preceding logic
10
11 // Retrieve Data:
12 String orderId = context.getEntity().getId();
13
14 GetOrderByIdQuery query = GetOrderByIdQuery.builder().id(orderId).build();
15 GetOrderByIdQuery.Data data = (GetOrderByIdQuery.Data) context.api().query(query);
16
17 RuleUtils.validateQueryResult(context, data, this.getClass());
18
19 // continuing logic...
20
21 }
22}`run` method is to perform any conditional logic required prior to building and producing an action.`totalPrice` of the Order is greater than a `threshold` parameter with a value of $100.1// imports & annotations...
2public class MyCustomRule implements Rule {
3
4 // local fields...
5
6 public void run(C context) {
7
8 // preceding logic...
9
10 // Simple Logic:
11 if (data.orderById().totalPrice() <= threshold) {
12 return;
13 }
14
15 // continuing logic...
16
17 }
18}1// imports & annotations...
2public class MyCustomRule implements Rule {
3
4 public void run(C context) {
5
6 // preceding logic...
7
8 // Prepare for Action:
9 AddAttributeToOrderMutation addAttributeToOrderMutation = AddAttributeToOrderMutation.builder()
10 .orderId(orderId)
11 .attributeName(IS_HIGH_VALUE_ORDER_ATTRIBUTE_NAME)
12 .attributeType(Attribute.Type.BOOLEAN.getValueClass().getSimpleName().toUpperCase())
13 .attributeValue(Boolean.TRUE)
14 .build();
15
16 // continuing logic...
17
18 }
19}`run` method is to produce the output action.1// imports & annotations...
2public class MyCustomRule implements Rule {
3
4 // local fields...
5
6 public void run(C context) {
7
8 // preceding logic...
9
10 // Produce Action:
11 context.action().mutation(addAttributeToOrderMutation);
12
13 }
14}Author:
Fluent Commerce
Changed on:
30 June 2024
`MyCustomRule` 1public class MyCustomRuleTest {
2
3 MyCustomRule rule = new MyCustomRule();
4
5 @Mock
6 Context context;
7
8 @Mock
9 Entity entity;
10
11 @Mock
12 ReadOnlyFluentApiClient apiClient;
13
14 @Mock
15 GetOrderByIdQuery.Data data;
16
17 @Mock
18 GetOrderByIdQuery.OrderById orderById;
19
20 @Mock
21 ActionFactory actionFactory;
22
23 @Before
24 public void setup() {
25
26 MockitoAnnotations.initMocks(this);
27
28 when(context.getProp(anyString())).thenReturn("1000");
29 when(context.getProp(RuleConstants.PARAM_NAME_HIGH_VALUE_THRESHOLD, Integer.class)).thenReturn(1000);
30 when(context.getEntity()).thenReturn(entity);
31 when(entity.getId()).thenReturn("123");
32 when(context.api()).thenReturn(apiClient);
33 when(apiClient.query(any())).thenReturn(data);
34 when(data.orderById()).thenReturn(orderById);
35 }
36
37 @Test(expected = MissingRequiredParamException.class)
38 public void MyCustomRule_withMissingThresholdParam_ThrowsMissingRequiredParamException() {
39
40 //arrange:
41 when(context.getProp(RuleConstants.PARAM_NAME_HIGH_VALUE_THRESHOLD)).thenReturn(null);
42
43 //act:
44 rule.run(context);
45
46 //assert:
47
48 }
49
50 @Test(expected = MissingRequiredParamException.class)
51 public void MyCustomRule_withEmptyThresholdParam_ThrowsMissingRequiredParamException() {
52
53 //arrange:
54 when(context.getProp(RuleConstants.PARAM_NAME_HIGH_VALUE_THRESHOLD)).thenReturn("");
55
56 //act:
57 rule.run(context);
58
59 //assert:
60
61 }
62
63 @Test
64 public void MyCustomRule_withValueGreaterThanThreshold_AddsAttributeHIGH_VALUE() {
65
66 //arrange:
67 when(orderById.totalPrice()).thenReturn(1001.00);
68 when(context.action()).thenReturn(actionFactory);
69
70 //act:
71 rule.run(context);
72
73 //assert:
74 verify(actionFactory, times(1)).mutation(any());
75
76 }
77
78 @Test
79 public void MyCustomRule_withValueLessThanThreshold_DoesNothing() {
80
81 //arrange:
82 when(orderById.totalPrice()).thenReturn(999.00);
83
84 //act:
85 rule.run(context);
86
87 //assert:
88 verify(actionFactory, never()).mutation(any());
89
90 }
91
92 @Test
93 public void MyCustomRule_withValueEqualThanThreshold_DoesNothing() {
94
95 //arrange:
96 when(orderById.totalPrice()).thenReturn(1000.00);
97
98 //act:
99 rule.run(context);
100
101 //assert:
102 verify(actionFactory, never()).mutation(any());
103
104 }
105}`TestExecutor` allows developers to set up a mock workflow for unit testing of specific rules and simulates the Rubix Orchestration Engine.It provides the following methods:`rule(Class.class)`: load a rule`ruleset(RuleSet ruleSet)`: adds a ruleset to the workflow`entity(Entity entity)`: adds an entity to the workflow`validateWorkflow(Event event)`: validates the workflow with the given event`execute(Event event)`: executes the workflow with the given event`TestContext``TestContext` provides functionality to retrieve results post-workflow execution.`action()`: Provides the action interfaces of the fluent retail API client.`api()`: Provides the API interfaces of the fluent retail API client.`count*()`: Provides access to the number of events or actions that have been executed in the context.`get*()`: Provides access to the entities, rules, events, and properties.`scanAndValidateAllRules()` Method`scanAndValidateAllRules()` unit test provided with the SDK, and should be present in each plugin. This validates the accuracy of the rule annotations and parameters.Author:
Fluent Commerce
Changed on:
14 Sept 2025
`RuleExecutionException`1public class ExampleRule implements Rule {
2
3 public <C extends Context> void run(C context) {
4
5 try {
6 // some rule code...
7 }
8 catch (Exception e) {
9 throw new CustomRuleException("Useful Message", e); // custom exception including the cause exception
10 }
11 }
12}`RuleExecutionException` which provides special handling of exceptions differently from all others. Any `RuleExecutionException`, or subclass thereof, thrown from or bubbled up through a rule back into the Workflow Engine Executor, will be handled as follows:`eventType` is set to `EXCEPTION``RuleExecutionEvent` message and the `cause` throwable (if it exists), will be available as part of the Exception Event for use within your rules1"rulesets": [
2 {
3 "name": "CancelOrder",
4 "type": "ORDER",
5 "subtype": "CC",
6 "eventType": "NORMAL",
7 "rules": [ ... ],
8 "triggers": [ ... ],
9 "userActions": []
10 },
11 {
12 "name": "CancelOrder",
13 "type": "ORDER",
14 "subtype": "CC",
15 "eventType": "EXCEPTION",
16 "rules": [ ... ],
17 "triggers": [ ... ],
18 "userActions": []
19 }
20]1{
2 "id": "8dcedfd0-a767-4066-8622-574f07cf092a",
3 "name": "ACME.custom2023.ThrowRuleExecutionExceptionRule",
4 "type": "ORCHESTRATION_AUDIT",
5 "accountId": "ACME",
6 "retailerId": "1",
7 "category": "rule",
8 "context": {
9 "sourceEvents": [
10 "e5de6a3b-0653-4df4-bd3d-ca3a83b1fcea"
11 ],
12 "entityType": "ORDER",
13 "entityId": "794",
14 "entityRef": "CC_4137",
15 "rootEntityType": "ORDER",
16 "rootEntityId": "794",
17 "rootEntityRef": "CC_4137"
18 },
19 "eventStatus": "FAILED",
20 "attributes": [
21 {
22 "name": "ruleSet",
23 "value": "TestExceptions",
24 "type": "STRING"
25 },
26 {
27 "name": "props",
28 "value": {},
29 "type": "STRING"
30 },
31 {
32 "name": "startTimer",
33 "value": 1689567378049,
34 "type": "STRING"
35 },
36 {
37 "name": "message",
38 "value": "Example of RuleExecutionException thrown from Rule",
39 "type": "STRING"
40 },
41 {
42 "name": "stopTimer",
43 "value": 1689567378059,
44 "type": "STRING"
45 }
46 ],
47 "source": null,
48 "generatedBy": "Rubix User",
49 "generatedOn": "2023-07-17T04:16:18.059+00:00"
50}1{
2 "id": "39ffb549-6e82-4e89-bc24-b1f2ec839e49",
3 "name": "java.lang.IllegalArgumentException",
4 "type": "ORCHESTRATION_AUDIT",
5 "accountId": "ACME",
6 "retailerId": "1",
7 "category": "exception",
8 "context": {
9 "sourceEvents": [
10 "48ef5c4e-8f29-4de0-9249-90f2c8cb5ae7"
11 ],
12 "entityType": "ORDER",
13 "entityId": "827",
14 "entityRef": "CC_5369",
15 "rootEntityType": "ORDER",
16 "rootEntityId": "827",
17 "rootEntityRef": "CC_5369"
18 },
19 "eventStatus": "FAILED",
20 "attributes": [
21 {
22 "name": "exception",
23 "value": {
24 "message": "Example of an Exception thrown from a Rule",
25 "stackTrace": [
26 {
27 "fileName": "ThrowOtherExceptionRule.java",
28 "className": "com.fluentcommerce.rule.ThrowOtherExceptionRule",
29 "lineNumber": 11,
30 "methodName": "run",
31 "nativeMethod": false,
32 "declaringClass": "com.fluentcommerce.rule.ThrowOtherExceptionRule"
33 },
34 // LOTS MORE STACKTRACE...
35 {
36 "fileName": "Thread.java",
37 "className": "java.lang.Thread",
38 "lineNumber": 750,
39 "methodName": "run",
40 "nativeMethod": false,
41 "declaringClass": "java.lang.Thread"
42 }
43 ],
44 "suppressed": [],
45 "classContext": [
46 "com.fluentcommerce.rule.ThrowOtherExceptionRule",
47 "com.fluentretail.rubix.plugin.registry.impl.BaseRuleProxyFactory$1",
48 "com.sun.proxy.$Proxy72",
49 "com.fluentretail.rubix.executor.EventExecutor",
50 "com.fluentretail.rubix.executor.EventExecutor",
51 "com.fluentretail.rubix.executor.EventExecutor",
52 "com.fluentretail.rubix.executor.EventExecutor",
53 "com.fluentretail.rubix.executor.EventExecutor$$Lambda$136/403856380",
54 "java.util.stream.MatchOps$1MatchSink",
55 "java.util.ArrayList$ArrayListSpliterator",
56 "java.util.stream.ReferencePipeline",
57 "java.util.stream.AbstractPipeline",
58 "java.util.stream.AbstractPipeline",
59 "java.util.stream.AbstractPipeline",
60 "java.util.stream.MatchOps$MatchOp",
61 "java.util.stream.MatchOps$MatchOp",
62 "java.util.stream.AbstractPipeline",
63 "java.util.stream.ReferencePipeline",
64 "com.fluentretail.rubix.executor.EventExecutor",
65 "com.fluentretail.rubix.executor.EventExecutor",
66 "com.fluentretail.rubix.executor.EventExecutor",
67 "com.fluentretail.rubix.executor.EventExecutor",
68 "com.fluentretail.rubix.executor.RubixEventHandler",
69 "com.fluentretail.rubix.executor.RubixEventHandler",
70 "org.apache.felix.ipojo.util.Callback",
71 "org.apache.felix.ipojo.handlers.event.subscriber.EventAdminSubscriberHandler",
72 "org.apache.felix.ipojo.handlers.event.subscriber.EventAdminSubscriberHandler",
73 "org.apache.felix.eventadmin.impl.handler.EventHandlerProxy",
74 "org.apache.felix.eventadmin.impl.tasks.HandlerTask",
75 "org.apache.felix.eventadmin.impl.tasks.SyncDeliverTasks",
76 "org.apache.felix.eventadmin.impl.handler.EventAdminImpl",
77 "org.apache.felix.eventadmin.impl.security.EventAdminSecurityDecorator",
78 "com.fluentretail.rubix.queue.EventActivator$1",
79 "com.fluentretail.rubix.queue.EventActivator$1$$Lambda$108/390688491",
80 "com.fluentretail.rubix.queue.impl.QueueListener",
81 "com.amazon.sqs.javamessaging.SQSSessionCallbackScheduler",
82 "java.util.concurrent.ThreadPoolExecutor",
83 "java.util.concurrent.ThreadPoolExecutor$Worker",
84 "java.lang.Thread"
85 ],
86 "detailMessage": "Example of an Exception thrown from a Rule",
87 "localizedMessage": "Example of an Exception thrown from a Rule",
88 "suppressedExceptions": []
89 },
90 "type": "OBJECT"
91 },
92 {
93 "name": "lastRule",
94 "value": "ACME.custom2023.ThrowOtherExceptionRule",
95 "type": "String"
96 },
97 {
98 "name": "lastRuleSet",
99 "value": "TestExceptions",
100 "type": "String"
101 },
102 {
103 "name": "message",
104 "value": "Example of an Exception thrown from a Rule",
105 "type": "String"
106 }
107 ],
108 "source": null,
109 "generatedBy": "Rubix User",
110 "generatedOn": "2023-07-17T04:21:53.439+00:00"
111}Author:
Fluent Commerce
Changed on:
5 Sept 2025