lib-bpmn-engine
lib-bpmn-engine copied to clipboard
support for backwards
Do we have any plan to support backwards ?
Problem: Assume that we allow user to repay the order instead of creating a new order when user choose to refund before confirm-receipt, it can handle confirm-recepit event and go to send-bill again. But it can't handle the next flow becasue of some event had already trigged. I can't clear or reset the processInfo either.
The order repay flow may sound weird, but it's common for me to desigin flows like that.
I'm trying to design a workflow that user can handle an event then supervisor recheck it. If the event handled before has some wrong, the supervisor can turn the flow to previous flow again. so that the previous user can handle it again until the supervisor recheck pass.
At now, I can't archive this goal graceful without backwards support. Do you have some suggestions?

Hi @lastchiliarch if I got you right, then you want to go backward, like in a loop? I mean basically, what does this loop BPMN shown below?
That is likely not supported in the current version ... but I will think about, how to implement it 👍
Thank you for your feedback. Also, PRs are always welcome :)
Yes, that's what i want. I'm new to bpmn so the implement is diffcult for me . But I'd like to help doing a part of job of this one or anything I can.
Hi @lastchiliarch
I've implemented a test, that show "backwards" works in this simple loop example above. See https://github.com/nitram509/lib-bpmn-engine/blob/532d2d18be8253b71d643fabd679d48e2170076a/pkg/bpmn_engine/engine_jobs_test.go#L23
I re-read your initial post and I have to admit, I don't understand your business requirement. What I can say, BPMN does not support "reset" == if you design some condition after the "confirm receipt", the bpmn flow will continue a second round.
This is likeley not what you want in business, I guess. I would rather suggest, to model a separate BPMN process for refunding.
A typical refind process could look like:
- find existing order process
- cancel existing order process (lib-bpmn-engine does not support cancelling yet, but BPMN itself is desgined for that)
- validate refund conditions
- subtract refund fees
- make payment order, so the user gets its money back
The benefit of having two processes are
- more transparency, what was happen, when you "link" the processes by their keys
- easier to understand
- easier to distribute
How does this resonate with you?
re-read your initial post and I have to admit, I don't understand your business requirement. What I can say, BPMN does not support "reset" == if you design some condition after the "confirm receipt", the bpmn flow will continue a second round.
Yes, I want reset function.
Some of my workflow likes below.

- user try to apply some auth
- reviewer audit the application and tag some label like city
- supervisor make the final confirm
- if supervisor think some label is not correct , he will ask the reviewer to re-audit it.
- reviewr re-aduit the tag updated label
- supervisor check the applcation again like step 4
- approve the auth if everything goes well
Since the enginer does not support reset, it will be traped into infinite loop if I don't clear the op variable in notifySupervisor. So What I want is clearing the processInfo where event is trigged in backwards case. If bpmn is not designed to support this, I'd like to have some functiones to clear or delete the processInfo variables manually to archive this goal.
Some is some of my test code and bpmn file.
var (
bpmnEngine = New("auth_apply")
process *ProcessInfo
instance *ProcessInstanceInfo
)
func Test_apply_auth_loop(t *testing.T) {
// setup
var err error
process, err = bpmnEngine.LoadFromFile("../../test-cases/auth_apply.bpmn")
bpmnEngine.AddTaskHandler("apply_auth", applyAuth)
bpmnEngine.AddTaskHandler("audit", audit)
bpmnEngine.AddTaskHandler("prove_auth", proveAuth)
bpmnEngine.AddTaskHandler("notify_supervisor", notifySupervisor)
instance, err = bpmnEngine.CreateInstance(process.ProcessKey, nil)
fmt.Println(err)
bpmnEngine.RunOrContinueInstance(instance.GetInstanceKey())
then.AssertThat(t, instance.GetVariable("city"), is.EqualTo("Beijing"))
then.AssertThat(t, instance.state, is.EqualTo(process_instance.COMPLETED))
}
func applyAuth(job ActivatedJob) {
log.Println("start to applyAuth")
job.Complete()
}
func audit(job ActivatedJob) {
log.Println("start to audit")
op := job.GetVariable("op")
if op == nil {
job.SetVariable("city", "London")
bpmnEngine.PublishEventForInstance(instance.GetInstanceKey(), "audit_by_reviewer")
job.Complete()
return
}
auditop := op.(string)
if auditop != "" {
job.SetVariable("city", "Beijing")
bpmnEngine.PublishEventForInstance(instance.GetInstanceKey(), "audit_by_reviewer")
job.Complete()
} else {
job.SetVariable("city", "London")
bpmnEngine.PublishEventForInstance(instance.GetInstanceKey(), "audit_by_reviewer")
job.Complete()
}
}
func proveAuth(job ActivatedJob) {
log.Println("start to proveAuth")
job.Complete()
}
func notifySupervisor(job ActivatedJob) {
log.Println("start to notifySupervisor")
city := job.GetVariable("city").(string)
if city != "Beijing" {
job.SetVariable("op", "re-audit")
} else {
job.SetVariable("op", "")
}
bpmnEngine.PublishEventForInstance(instance.GetInstanceKey(), "confirm_by_supervisor")
job.Complete()
}
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_1l2k2zy" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.2.0" modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.0.0">
<bpmn:process id="Process_16ue9kj" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0fwz2ln</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:serviceTask id="prove_auth" name="prove_auth">
<bpmn:extensionElements>
<zeebe:taskDefinition type="prove_auth" />
</bpmn:extensionElements>
<bpmn:incoming>Flow_0bqwngy</bpmn:incoming>
<bpmn:outgoing>Flow_0l4cgrv</bpmn:outgoing>
</bpmn:serviceTask>
<bpmn:endEvent id="Event_0jv5wxq">
<bpmn:incoming>Flow_0l4cgrv</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0bqwngy" sourceRef="Gateway_1dmvvpa" targetRef="prove_auth" />
<bpmn:serviceTask id="apply_auth" name="apply_auth">
<bpmn:extensionElements>
<zeebe:taskDefinition type="apply_auth" />
</bpmn:extensionElements>
<bpmn:incoming>Flow_0fwz2ln</bpmn:incoming>
<bpmn:outgoing>Flow_1lr5ur1</bpmn:outgoing>
</bpmn:serviceTask>
<bpmn:intermediateCatchEvent id="confirm_by_supervisor" name="confirm_by_supervisor">
<bpmn:incoming>Flow_177ps1y</bpmn:incoming>
<bpmn:outgoing>Flow_0wvvvjc</bpmn:outgoing>
<bpmn:messageEventDefinition id="MessageEventDefinition_1mr6cvo" messageRef="Message_1agfbps" />
</bpmn:intermediateCatchEvent>
<bpmn:intermediateCatchEvent id="audit_by_reviewer" name="audit_by_reviewer">
<bpmn:incoming>Flow_1grwq17</bpmn:incoming>
<bpmn:outgoing>Flow_11yboz4</bpmn:outgoing>
<bpmn:messageEventDefinition id="MessageEventDefinition_0j6i8wh" messageRef="Message_27ugmpv" />
</bpmn:intermediateCatchEvent>
<bpmn:sequenceFlow id="Flow_0wvvvjc" sourceRef="confirm_by_supervisor" targetRef="Gateway_1dmvvpa" />
<bpmn:parallelGateway id="Gateway_07ivcgb">
<bpmn:incoming>Flow_11yboz4</bpmn:incoming>
<bpmn:outgoing>Flow_06la618</bpmn:outgoing>
</bpmn:parallelGateway>
<bpmn:sequenceFlow id="Flow_11yboz4" sourceRef="audit_by_reviewer" targetRef="Gateway_07ivcgb" />
<bpmn:eventBasedGateway id="Gateway_1t2zx79">
<bpmn:incoming>Flow_0e397yl</bpmn:incoming>
<bpmn:outgoing>Flow_1grwq17</bpmn:outgoing>
</bpmn:eventBasedGateway>
<bpmn:sequenceFlow id="Flow_1grwq17" sourceRef="Gateway_1t2zx79" targetRef="audit_by_reviewer" />
<bpmn:serviceTask id="notify_supervisor" name="notify_supervisor">
<bpmn:extensionElements>
<zeebe:taskDefinition type="notify_supervisor" />
</bpmn:extensionElements>
<bpmn:incoming>Flow_06la618</bpmn:incoming>
<bpmn:outgoing>Flow_0c8hcd4</bpmn:outgoing>
</bpmn:serviceTask>
<bpmn:sequenceFlow id="Flow_06la618" sourceRef="Gateway_07ivcgb" targetRef="notify_supervisor" />
<bpmn:eventBasedGateway id="Gateway_00r4evy">
<bpmn:incoming>Flow_0c8hcd4</bpmn:incoming>
<bpmn:outgoing>Flow_177ps1y</bpmn:outgoing>
</bpmn:eventBasedGateway>
<bpmn:sequenceFlow id="Flow_0c8hcd4" sourceRef="notify_supervisor" targetRef="Gateway_00r4evy" />
<bpmn:sequenceFlow id="Flow_177ps1y" sourceRef="Gateway_00r4evy" targetRef="confirm_by_supervisor" />
<bpmn:sequenceFlow id="Flow_0l4cgrv" sourceRef="prove_auth" targetRef="Event_0jv5wxq" />
<bpmn:sequenceFlow id="Flow_1lr5ur1" sourceRef="apply_auth" targetRef="audit" />
<bpmn:sequenceFlow id="Flow_0e397yl" sourceRef="audit" targetRef="Gateway_1t2zx79" />
<bpmn:sequenceFlow id="re_audit" name="op='re-audit"" sourceRef="Gateway_1dmvvpa" targetRef="audit">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=op matches "re-audit"</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:serviceTask id="audit" name="audit">
<bpmn:extensionElements>
<zeebe:taskDefinition type="audit" />
</bpmn:extensionElements>
<bpmn:incoming>Flow_1lr5ur1</bpmn:incoming>
<bpmn:incoming>re_audit</bpmn:incoming>
<bpmn:outgoing>Flow_0e397yl</bpmn:outgoing>
</bpmn:serviceTask>
<bpmn:sequenceFlow id="Flow_0fwz2ln" sourceRef="StartEvent_1" targetRef="apply_auth" />
<bpmn:exclusiveGateway id="Gateway_1dmvvpa">
<bpmn:incoming>Flow_0wvvvjc</bpmn:incoming>
<bpmn:outgoing>Flow_0bqwngy</bpmn:outgoing>
<bpmn:outgoing>re_audit</bpmn:outgoing>
</bpmn:exclusiveGateway>
</bpmn:process>
<bpmn:message id="Message_27ugmpv" name="audit_by_reviewer">
<bpmn:extensionElements>
<zeebe:subscription correlationKey="=key" />
</bpmn:extensionElements>
</bpmn:message>
<bpmn:message id="Message_067eof3" name="Message_067eof3">
<bpmn:extensionElements>
<zeebe:subscription correlationKey="=key" />
</bpmn:extensionElements>
</bpmn:message>
<bpmn:message id="Message_1agfbps" name="confirm_by_supervisor">
<bpmn:extensionElements>
<zeebe:subscription correlationKey="=key" />
</bpmn:extensionElements>
</bpmn:message>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_16ue9kj">
<bpmndi:BPMNEdge id="Flow_0fwz2ln_di" bpmnElement="Flow_0fwz2ln">
<di:waypoint x="168" y="177" />
<di:waypoint x="200" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_199ps6v_di" bpmnElement="re_audit">
<di:waypoint x="1000" y="152" />
<di:waypoint x="1000" y="90" />
<di:waypoint x="390" y="90" />
<di:waypoint x="390" y="137" />
<bpmndi:BPMNLabel>
<dc:Bounds x="665" y="72" width="63" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0e397yl_di" bpmnElement="Flow_0e397yl">
<di:waypoint x="440" y="177" />
<di:waypoint x="475" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1lr5ur1_di" bpmnElement="Flow_1lr5ur1">
<di:waypoint x="300" y="177" />
<di:waypoint x="340" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0l4cgrv_di" bpmnElement="Flow_0l4cgrv">
<di:waypoint x="1190" y="177" />
<di:waypoint x="1272" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_177ps1y_di" bpmnElement="Flow_177ps1y">
<di:waypoint x="865" y="177" />
<di:waypoint x="892" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0c8hcd4_di" bpmnElement="Flow_0c8hcd4">
<di:waypoint x="790" y="177" />
<di:waypoint x="815" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_06la618_di" bpmnElement="Flow_06la618">
<di:waypoint x="665" y="177" />
<di:waypoint x="690" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1grwq17_di" bpmnElement="Flow_1grwq17">
<di:waypoint x="525" y="177" />
<di:waypoint x="552" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_11yboz4_di" bpmnElement="Flow_11yboz4">
<di:waypoint x="588" y="177" />
<di:waypoint x="615" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0wvvvjc_di" bpmnElement="Flow_0wvvvjc">
<di:waypoint x="928" y="177" />
<di:waypoint x="975" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0bqwngy_di" bpmnElement="Flow_0bqwngy">
<di:waypoint x="1025" y="177" />
<di:waypoint x="1090" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="132" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0d43kkb_di" bpmnElement="apply_auth">
<dc:Bounds x="200" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_05mf2rz_di" bpmnElement="confirm_by_supervisor">
<dc:Bounds x="892" y="159" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="865" y="121.5" width="89" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_001ghjo_di" bpmnElement="audit_by_reviewer">
<dc:Bounds x="552" y="159" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="527" y="202" width="86" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1mfbkhy_di" bpmnElement="Gateway_07ivcgb">
<dc:Bounds x="615" y="152" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1u2qqcb_di" bpmnElement="Gateway_1t2zx79">
<dc:Bounds x="475" y="152" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1be98t1_di" bpmnElement="notify_supervisor">
<dc:Bounds x="690" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1kgw3tw_di" bpmnElement="Gateway_00r4evy">
<dc:Bounds x="815" y="152" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1doq7r3_di" bpmnElement="audit">
<dc:Bounds x="340" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0jv5wxq_di" bpmnElement="Event_0jv5wxq">
<dc:Bounds x="1272" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0zsfe4z_di" bpmnElement="prove_auth">
<dc:Bounds x="1090" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_0vfgvoa_di" bpmnElement="Gateway_1dmvvpa" isMarkerVisible="true">
<dc:Bounds x="975" y="152" width="50" height="50" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
Got your point ... there seems to be another bug, working on it.
Hi siconghuang,
I did just a minute ago post some fixes, which were still present, for the bug mentioned above.
I did some modifications as well, to make the code + BPMN more simple.
- IF there's just a single intermediate message catch event in the flow, you don't need a event gateway in front neither an exclusive gateway afterward
- you should not send events from inside a service task
- that might be technically possible, but is not necessary code, the engine does the process execution for you
- it's very difficult to read/understand such code
- from your example, I think it would be more convenient to have the #41 implemented, BUT as you can see, the "validate supervisor result" task takes care of updating the proper variable within the instance.

import (
"fmt"
"github.com/corbym/gocrest/is"
"github.com/corbym/gocrest/then"
"github.com/nitram509/lib-bpmn-engine/pkg/bpmn_engine"
"github.com/nitram509/lib-bpmn-engine/pkg/spec/BPMN20/process_instance"
"log"
"testing"
)
var (
bpmnEngine = bpmn_engine.New("auth_apply")
process *bpmn_engine.ProcessInfo
instance *bpmn_engine.ProcessInstanceInfo
)
// these test-* variables do mimic/simulate external events or manual actions
var testAuditAttempt = 0
const BEIJING = "Beijing"
const LONDON = "London"
func Test_apply_auth_loop(t *testing.T) {
// setup
var err error
process, err = bpmnEngine.LoadFromFile("auth_apply.bpmn")
bpmnEngine.AddTaskHandler("apply_auth", applyAuth)
bpmnEngine.AddTaskHandler("audit", audit)
bpmnEngine.AddTaskHandler("notify_supervisor", notifySupervisor)
bpmnEngine.AddTaskHandler("validate_supervisor_result", validateSupervisorResult)
bpmnEngine.AddTaskHandler("prove_auth", proveAuth)
instance, err = bpmnEngine.CreateInstance(process.ProcessKey, nil)
fmt.Println(err)
// HINT: BPMN spec allows message published with variables, but the engine does not (yet) support that
// so ideally, bpmnEngine.PublishEventForInstance() should support variables to pass in
bpmnEngine.RunOrContinueInstance(instance.GetInstanceKey())
then.AssertThat(t, instance.GetVariable("city"), is.EqualTo(LONDON))
bpmnEngine.PublishEventForInstance(instance.GetInstanceKey(), "supervision_reported") // first response from supervisor
bpmnEngine.RunOrContinueInstance(instance.GetInstanceKey()) // will loop back and executes the audit as well, stops because of second intermediate catch event
bpmnEngine.PublishEventForInstance(instance.GetInstanceKey(), "supervision_reported") // first response from supervisor
bpmnEngine.RunOrContinueInstance(instance.GetInstanceKey()) // will catch up the second event and exits the instance
then.AssertThat(t, instance.GetVariable("city"), is.EqualTo(BEIJING))
then.AssertThat(t, instance.GetState(), is.EqualTo(process_instance.COMPLETED))
}
func validateSupervisorResult(job bpmn_engine.ActivatedJob) {
reAudit := testAuditAttempt <= 1 // for testing purpose, the first attempt is always wrong
log.Println(fmt.Sprintf("validateSupervisorResult, reAudit=%t", reAudit))
job.SetVariable("reAudit", reAudit)
job.Complete()
}
func applyAuth(job bpmn_engine.ActivatedJob) {
log.Println("start to applyAuth")
job.Complete()
}
func audit(job bpmn_engine.ActivatedJob) {
log.Println("start to audit")
if testAuditAttempt < 1 {
job.SetVariable("city", LONDON)
} else {
job.SetVariable("city", BEIJING)
}
testAuditAttempt = testAuditAttempt + 1 // change audit result for next time
job.Complete()
}
func proveAuth(job bpmn_engine.ActivatedJob) {
log.Println("start to proveAuth")
job.Complete()
}
func notifySupervisor(job bpmn_engine.ActivatedJob) {
city := job.GetVariable("city").(string)
log.Println(fmt.Sprintf("start to notifySupervisor; city=%s", city))
job.Complete()
}
BPMN Source ... auth_apply.bpmn.txt
@lastchiliarch does my above proposal help?
@lastchiliarch does my above proposal help?
Thanks for your help and hard work. It's diffcult for the "validate supervisor result" task to control the flow, seems message event could be a more graceful approach.