bpmnServer / bpmn-server

BPMN 2.0 server for Node.js , providing modeling, execution, persistence and monitoring for Workflow. along with sample UI. Intended to be developers workbench for BPMN 2.0
MIT License
183 stars 47 forks source link

different Between Invoke & Task Listener #146

Closed kstan79 closed 11 months ago

kstan79 commented 11 months ago

According BPMN concept.

  1. before/after execution support all element
  2. task listener only support user task
  3. implementation -> delegate for user task

I wish to ask does bpmn-server support task listener? if yes is the method same like service task? Cause we need to read data submited by invoke api.

For me, invoke user-task happen at client side, task listener for user-task launch at server side. Example:

<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn">
  <bpmn:process id="process1" name="Suspend Customer" isExecutable="false">
    <bpmn:extensionElements />
    <bpmn:startEvent id="StartEvent_1">
      <bpmn:outgoing>Flow_1a63g0z</bpmn:outgoing>
    </bpmn:startEvent>
    <bpmn:endEvent id="Event_0qpgd23">
      <bpmn:incoming>Flow_1g3iygg</bpmn:incoming>
    </bpmn:endEvent>
    <bpmn:sequenceFlow id="Flow_1x9zeai" sourceRef="approve1" targetRef="Activity_0pu51x5" />
    <bpmn:sequenceFlow id="Flow_1a63g0z" sourceRef="StartEvent_1" targetRef="approve1" />
    <bpmn:userTask id="approve1" name="approve by supervisor" camunda:formKey="jsonschema://SimpleApproveReject" camunda:assignee="kstan">
      <bpmn:extensionElements>
        <camunda:taskListener delegateExpression="afterInvoke" event="create" />
      </bpmn:extensionElements>
      <bpmn:incoming>Flow_1a63g0z</bpmn:incoming>
      <bpmn:outgoing>Flow_1x9zeai</bpmn:outgoing>
    </bpmn:userTask>
    <bpmn:sequenceFlow id="Flow_1g3iygg" sourceRef="Activity_0pu51x5" targetRef="Event_0qpgd23" />
    <bpmn:serviceTask id="Activity_0pu51x5" name="hello" camunda:delegateExpression="system.hello">
      <bpmn:incoming>Flow_1x9zeai</bpmn:incoming>
      <bpmn:outgoing>Flow_1g3iygg</bpmn:outgoing>
    </bpmn:serviceTask>
  </bpmn:process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="process1">
      <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
        <dc:Bounds x="122" y="202" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Event_0qpgd23_di" bpmnElement="Event_0qpgd23">
        <dc:Bounds x="672" y="202" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_1odv9ki_di" bpmnElement="approve1">
        <dc:Bounds x="200" y="180" width="100" height="80" />
        <bpmndi:BPMNLabel />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_1tksj2t_di" bpmnElement="Activity_0pu51x5">
        <dc:Bounds x="520" y="180" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="Flow_1x9zeai_di" bpmnElement="Flow_1x9zeai">
        <di:waypoint x="300" y="220" />
        <di:waypoint x="520" y="220" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_1a63g0z_di" bpmnElement="Flow_1a63g0z">
        <di:waypoint x="158" y="220" />
        <di:waypoint x="200" y="220" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_1g3iygg_di" bpmnElement="Flow_1g3iygg">
        <di:waypoint x="620" y="220" />
        <di:waypoint x="672" y="220" />
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</bpmn:definitions>
ralphhanna commented 11 months ago

Regarding Task Listener

Cause we need to read data submited by invoke api.

Don't understand, your own app invoke the api ? and provide data to api

ralphhanna commented 11 months ago

To Illustrate some more about events sequence:

Model is: StartEvent_1 Request:user Task boundaryEvent Approve:user Task

image Sequence of Execution:

---calling start----- ---Event: --> process.start ---Event: -->StartEvent_1 transformInput ---Event: -->StartEvent_1 enter ---Event: -->StartEvent_1 start ---Event: --> process.saving ---Event: -->StartEvent_1 end ---Event: -->Request enter ---Event: -->Request start* ---Event: -->Request wait ---Event: --> process.wait ---Event: --> process.saving

---calling assign----- ---Event: --> process.restored ---Event: -->Request assignment* ---Event: --> process.saving

---calling invoke----- ---Event: --> process.restored ---Event: --> process.invoke

---Event: -->Request transformInput ---Event: -->Request end*

---Event: -->Approve enter ---Event: -->Approve start ---Event: -->boundaryEvent enter ---Event: -->boundaryEvent start ---Event: -->boundaryEvent wait ---Event: -->Approve wait ---Event: --> process.invoked ---Event: --> process.saving

kstan79 commented 11 months ago

hm... there is a few comment at the diagram (which is side topic)

  1. I thought task allow only 1 in and 1 out. Your request having 2 incoming, which should connect using gateway ?
  2. reminder script task no output ok? I thought every branch need end properly.

Come back to topic:

  1. we have single set common workflows api, which involve start workflow, invoke workflow
  2. all bpmn run the start and invoke using same api, with different parameters
  3. Use user task as example, we call invoke from remote api client (like swagger-ui/postman) a. since bpmn server hosted at backend, bpmn client only can submit api call together with data. Example post http://localhost:8000/api/invoke/suspendcustomer b. at server side, we create a some delegate function likedelegateSupervisorApprove, and wish to bind at user-task:
    deligatesample c. since frontend have no knowledge what is the workflow behind, so it will just call invoke, show job done to users. d. at backend, user task involve many flow as below (which i feel task listener and execution redundant with), and I try to give feasible implementation of each type i) workflow init user task (create) -> log into database, obtain some runtime variables ii) workflow resolve user/group (assignment) -> resolve dynamic user become real person, send notification to users iii) after user task invoke by user (complete) -> receive input from user, task completed, but yet to go next step (example we may want to reverse back to previous status due to validation error) iv) after user task end, no longer under 'wait' (delete), send notification to process initiator say someone proceed to next step successfully beside direct flow, i think below is special flow? v) update (not invoke workflow, changing of user task properties only), like swap users, change due date, change priority vi) timeout (as we know this is use by timer).

My opinion: base on observation, seems all above action make sense (not only assign), and correct me if I'm wrong.

  1. (i-iv) will execute after invoke, follow sequence.
  2. seems theassign you mentioned will trigger (v)? If this is the case seems assign need rename to avoid confusion.
  3. (vi) i not yet test and no comment

Conclusion:

1. I suggest all the event type (i-vi) just follow same way as service task.  put user invoke's `vars/data` & context inside
2. ignore `field injection`, as it is pandora box same like `form`
3. may rename `assign` to `update` if appropriate
ralphhanna commented 11 months ago

Regarding the Diagram above:

  1. Require Information sequenceFlow, initiate a new instance of the Request Task image

  2. Reminder has no impact on the workflow, i.e. does not terminate or end, but you can have non-terminating end event if you like.

ralphhanna commented 11 months ago

Regarding Sequence of logic, currently bpmn-server support the following:

Start - Based on some user input or some other business logic:

  1. App starts the Workflow ( by calling api.engine.start(...) a. Workflow instance is created b. Workflow executes
  2. Once a User Task is reached: a. Item created
    b. Item initialized c. start execution delegate is called d. Item goes into wait state e. Workflow is saved

So far, data and assignment are not done, unless created by start

Assign: Based on some user input

  1. App may call api.engine.assign(...,assignment) a. Workflow is restored b. set assignment as passed c. 'assignment` task delegate is called d. Workflow is save

No impact on workflow state

Invoke: Based on some user input or some other business logic:

  1. App call api.engine.invoke(..,data) a. data is appended c. end execution delegate is called d. Task is now complete, status =end e. Workflow proceeds with execution

For Service Task and Similar

  1. App starts the Workflow ( by calling api.engine.start(...) a. Workflow instance is created b. Workflow executes
  2. Once a Service Task is reached: a. Item created
    b. Item initialized c. start execution delegate is called d. Service Task is invoked using delegate specified e. end execution delegate is called f. Workflow proceeds with execution

In Conclusion:

Thanks

kstan79 commented 11 months ago

I try to understand your reply but i think we are talking different thing. I recommend we shall support all task listener event type, you reply current design pattern.

I notice that invoke maintain in Engine.ts, but the actual effected place is Execution.ts. There is some confusion and naming terminology and design, and I try to propose easiest implementation way I can imagine.

  1. base on below enum we can use switch/case trigger suitable task listener
    
    enum ITEM_STATUS {
    enter = 'enter',
    start = 'start',
    wait = 'wait',
    end = 'end',
    terminated = 'terminated',
    cancelled = 'cancelled',
    discard = 'discard'

} enum EXECUTION_EVENT { node_enter = 'enter', node_start = 'start', node_wait = 'wait', node_end = 'end', node_terminated = 'terminated', transform_input = 'transformInput', transform_output ='transformOutput', flow_take = 'take', flow_discard = 'discard', process_loaded ='process.loaded', process_start = 'process.start',
process_started = 'process.started', process_invoke = 'process.invoke',
process_invoked = 'process.invoked', process_saving = 'process.saving', process_restored = 'process.restored', process_resumed = 'process_resumed', process_wait = 'process.wait', process_end = 'process.end', process_terminated = 'process.terminated' , token_start = 'token.start', token_wait = 'token.wait', token_end = 'token.end', token_terminated = 'token.terminated' }

create new enum specific for user-task TASKLISTENER
```ts
enum TASKLISTENER_EVENT{
   create='create', 
   assign='assign', 
   complete='complete',
   delete='delete',
   update='update',
   timeout='timeout' // may not have any place to track timeout yet
}
  async doExecutionEvent(process,event) {
        //this.item = null;        
        await this.listener.emit(event, { event, context: this });

        switch(event){
           case EXECUTION_EVENT.invoke:
                   await this.listener.emit(event, { TASKLISTENER_EVENT.complete, context: this });
           break;
           case. EXECUTION_EVENT.invoked:
                   await this.listener.emit(event, { TASKLISTENER_EVENT.delete, context: this });                
           break;
        }
        await this.listener.emit('all', { event, context: this });
    }

 async doItemEvent(item, event) {
        this.item = item;
        await this.listener.emit(event, { event, context: this });
        // I don't know event is what type, i assume  is "ITEM_STATUS"
        switch(event){
           case. ITEM_STATUS.enter: //correct me if im wrong
                   await this.listener.emit(event, { TASKLISTENER_EVENT.create, context: this });
           break;
           case. ITEM_STATUS.start:  //correct me if im wrong
                   await this.listener.emit(event, { TASKLISTENER_EVENT.assign, context: this });                
           break;
          // some others
        }
        await this.listener.emit('all', { event, context: this });
    }

above is just ideal i can think with, However, the issue is I don't know where to obtain user's input, so it is only an example

ralphhanna commented 11 months ago
  1. User`s input is currently provided at the three calls:
    • engine.start at the process level not item
    • engine.assign at the item level, you can call it update, except it accepts assignment info, has no impact on item status
    • engine.invoke at the item level, completes the Task
  2. Where in the sequence of events I have listed before would invoke your proposed Task Delegates
    • create: is it same as current start
    • assign: at current assignment
    • complete is it same as current end
    • delete: What is that, deleting what?
    • update: when does it happen? after assign?
    • timeout: what is that ? timeout of what? is it the dueDate or followUpDate?

I would appreciate your response to these questions. Thanks

kstan79 commented 11 months ago

delete: i don't know, in fact after some google i understand wrongly, read here update: I think after assign, agree that timeout: read here maybe helpful

ralphhanna commented 11 months ago

I am still not clear what is the purpose of delete , update and timeout , I think these are very specific to Camunda complex implementation. The only one task listeners that I can think of :

kstan79 commented 11 months ago

base on the discussion, seems [delete] when we delete/cancel a process, all the tasks within the bpmn will trigger 'delete' event. I think this is make sense, cause it can notify related users who'd initiate/approve/did something about the task.Simple exmaple of truck rentalk process having 1 user task instruct driver go to sides, if the enire sales suddenly beeen cancel then the task deleted, he been notify no need to go client side.

[timeout]

The timeout event occurs when a Timer, associated with this Task Listener, is due. Note that this requires for a Timer to be defined. The timeout event may occur after a Task has been created, and before it has been completed. It seems it attach a timer to that user task (cause the configuration exactly same like timer), if current time is over the dueDate then it trigger timeOut event. It is common practise in industry for escalate work.

Example: sales agent shall issue quotation to customer after enquiry receive in 2 days (due date 2 days), after 2 days if this job not completed it will trigger notification to supervisor. Every day (cycle 1 day) the supervisor receive system msg remind to chase after the sales agent

[update]

The update event occurs when a task property (e.g. assignee, owner, priority, etc.) on an already created task is changed. This includes attributes of a task (e.g. assignee, owner, priority, etc.), as well as dependent entities (e.g. attachments, comments, task-local variables). Note that the initialization of a task does not fire an update event (the task is being created). This also means that the update event will always occur after a create event has already occurred

seen task property changed, but not status change

ralphhanna commented 11 months ago
  1. Cancel Task is not a valid operation on task, there needs to be another Event defined in the workflow to cancel the entire workflow. image
  2. timeout, yes one can define boundary events as below: image. No sense of notify the task of timeout, it is the role of the task following the time to take action, in this case , Reminder

The Challenge with that is the attached timer event, it is evaluated and starts at the start of the task, before assignment and dueDate is set. (see my solution below)

  1. Are you suggesting that bpmn-server fires an timer event automatically at dueDate and followUpDate?
  2. update, would make sense if we allow users to update the task, but we don't. The only options currently is through
    engine.assign(..,data,assignment,...)

    Which fires the trigger 'assign'

I am keen on wrapping this up to get ready for next release so, here is my proposal:

Changes to current design

  1. engine.assign would be used to change assignment and change data
  2. The only Task delegates would be
    • assign : Fired only after the engine.assign
    • validate: Fired after engine.assign and 'engine.invoke`
  3. validate delegate would re-evaluate the boundary event timer and re-execute it, this would allow you to escalate issues based on the revised dueDate and followUpDate.
  4. Please see my previous notes about Notifications

Thanks

kstan79 commented 11 months ago
  1. Cancel Task is not a valid operation on task, there needs to be another Event defined in the workflow to cancel the entire workflow.

yes there is alternative by draw another another service task, It is not must have, but with this can reduce number of element in bpmn. We may look into this again future, cause 1 day we use camunda-modeler, 1 day it will remind us bpmn-server not compatible with bpmn-editor. Future either:

  1. timeout, yes one can define boundary events as below: No sense of notify the task of timeout, it is the role of the task following the time to take action, in this case , Reminder

similar as above, not must have

The Challenge with that is the attached timer event, it is evaluated and starts at the start of the task, before assignment and dueDate is set. (see my solution below)

  1. Are you suggesting that bpmn-server fires an timer event automatically at dueDate and followUpDate? Since we use camunda modelere, I always recommend having same behavior with the modeler concept to avoid confusion. In this case, yes I suggest add a timer. However it no high urgency cause next release too much changes, we may scope down and plan it using milestone

  2. update, would make sense if we allow users to update the task, but we don't. The only options currently is through

engine.assign(..,data,assignment,...)

Which fires the trigger 'assign'

no problem, may create document of unsupported features/event.

I am keen on wrapping this up to get ready for next release so, here is my proposal:

Changes to current design

  1. engine.assign would be used to change assignment and change data
  2. The only Task delegates would be
  • assign : Fired only after the engine.assign
  • validate: Fired after engine.assign and 'engine.invoke`
  1. validate delegate would re-evaluate the boundary event timer and re-execute it, this would allow you to escalate issues based on the revised dueDate and followUpDate.
  2. Please see my previous notes about Notifications

Thanks

I have feed back as below:

  1. we need a complete event, in this case is process_invoked? it is complete anyway. may write in documentation.
  2. we need a event during invoke (or, before), is it process_invoke?
  3. if you want an dedicate validate event, I'm ok
  4. base on camunda documentation, seems update & assignment quite similar and we may only use 1 of them, or put update as synonym of assign. After all tidy up we can write in document afterward
ralphhanna commented 11 months ago

Thanks for the Feedback, so here is the revised list of sequences

Sequence of Execution:

---calling engine.start-----

---Event: -->process.start { option1: 1234 }
---Event: -->transformInput item: StartEvent_1 { option1: 1234 }
---Event: -->enter item: StartEvent_1 { option1: 1234 }
---Event: -->start item: StartEvent_1 { option1: 1234 }
---Event: -->process.saving { option1: 1234 }
---Event: -->end item: StartEvent_1 { option1: 1234 }
---Event: -->enter item: Request { option1: 1234 }
---Event: -->start item: Request { option1: 1234 }
---Event: -->wait item: Request { option1: 1234 }
---Event: -->process.wait { option1: 1234 }
---Event: -->process.saving { option1: 1234 }

---calling engine.assign-----

---Event: -->process.restored { option1: 1234, restored: true }
---Event: -->assign item: Request { option1: 1234, restored: true }
---Event: -->validate item: Request { option1: 1234, restored: true }
---Event: -->process.saving { option1: 1234, restored: true }

---calling engine.invoke----- for Request

---Event: -->process.restored { option1: 1234, restored: true }
---Event: -->process.invoke { option1: 1234, restored: true }
---Event: -->transformInput item: Request { option1: 1234, restored: true }
---Event: -->validate item: Request { option1: 1234, restored: true }
---Event: -->end item: Request { option1: 1234, restored: true }
---Event: -->enter item: Approve { option1: 1234, restored: true }
---Event: -->start item: Approve { option1: 1234, restored: true }
---Event: -->enter item: Event_1lkpj3z { option1: 1234, restored: true }
---Event: -->start item: Event_1lkpj3z { option1: 1234, restored: true }
---Event: -->wait item: Event_1lkpj3z { option1: 1234, restored: true }
---Event: -->wait item: Approve { option1: 1234, restored: true }
---Event: -->process.invoked { option1: 1234, restored: true }
---Event: -->process.saving { option1: 1234, restored: true }

---calling engine.invoke----- for Approve

---Event: -->process.restored { option1: 1234, restored: true }
---Event: -->process.invoke { option1: 1234, restored: true }
---Event: -->transformInput item: Approve { option1: 1234, restored: true }
---Event: -->validate item: Approve { option1: 1234, restored: true }
---Event: -->end item: Event_1lkpj3z { option1: 1234, restored: true }
---Event: -->end item: Approve { option1: 1234, restored: true }
---Event: -->enter item: Gateway_1kqewfd { option1: 1234, restored: true }
---Event: -->start item: Gateway_1kqewfd { option1: 1234, restored: true }
---Event: -->process.saving { option1: 1234, restored: true }
---Event: -->end item: Gateway_1kqewfd { option1: 1234, restored: true }
---Event: -->enter item: Activity_1rx1txe { option1: 1234, restored: true }
---Event: -->start item: Activity_1rx1txe { option1: 1234, restored: true }
---Event: -->process.saving { option1: 1234, restored: true }
---Event: -->end item: Activity_1rx1txe { option1: 1234, restored: true }
---Event: -->enter item: Event_1qg3mz1 { option1: 1234, restored: true }
---Event: -->start item: Event_1qg3mz1 { option1: 1234, restored: true }
---Event: -->process.saving { option1: 1234, restored: true }
---Event: -->end item: Event_1qg3mz1 { option1: 1234, restored: true }
---Event: -->process.end { option1: 1234, restored: true }
---Event: -->process.invoked { option1: 1234, restored: true }
---Event: -->process.saving { option1: 1234, restored: true }

Commands:

kstan79 commented 11 months ago

Task listeners: only for User Tasks, , fires JavaScripts defined at the model

we can't support delegate too? which able to execute in source code in appDelegate. Javascript editor is really bad implementation for production quality. no syntax highlight, no error checking.

assign: invoked on invoke and assign commands validate: invoked on invoke and assign commands

regarding task listener, it is recommended specific event bind to specific method to prevent confusion (Due to I can't imagine how to bind the event yet). Or, will it have additional property which can tell us what the specific actual event trigged (invoke, invoked, or assign)?

Event listener: is only in the application code (typically appDelegate)

hard to imagine yet. you have sample code? Which will be code to illustrate your deisgn pattern

ralphhanna commented 11 months ago
  1. Regarding your concern about javascript editor in the model editor, you can keep them very simple one liner to call appUtils where you have full JS and TS.
  2. Keep in mind that there are 3 parameters here:
    • Model Name
    • Element
    • Event I have done an implementation where the class bind to the model name LeaveApplication.ts But methods are named approve_start , approve_validate, etc. This can be done by your own appDelegate, if interested, let us start a new discussion, this one is getting too long.
  3. Below is the latest document regarding this topic: scripting docs
kstan79 commented 11 months ago

i'd went through slightly the documentation. You may proceed what you wish to do. after come out latest version then I try to make it support delegate.

ralphhanna commented 11 months ago

Thanks, I am closing this, lf any issues start another