Dativity
Dativity is a stateless, data driven workflow engine library for Clojure and ClojureScript.
It is inspired by the Artifact centric business process model.
Table of Contents
Motivation
Design
Features
Examples
Dependencies
License
Latest version
See Changelog for release contents.
Motivation
Conventional process engines such as Activiti and Camunda are centered around the sequence in which activities should be performed, often modeled using BPMN.
The key idea of dativity is that the progression of data is the main driver of a workflow. It is achieved by revealing what actions can be performed given how actions depend on data and what data is currently available, rather than what actions should be performed.
For example, an insurance claim can not be reviewed until it has been submitted. The action of submitting produces data - the claim, and the review action depends on it. As soon as there is a claim, it's possible to perform the review action.
Many business process tools require a database to track the progress of a workflow. By adopting the Artifact centric business process model, dativity is able to provide the same core value as those other tools without requiring a dedicated database. Deciding how data is stored is up to you.
By being stateless, dativity allows you to reduce the complexity of your application.
This blog post offers a more extensive motivation.
Design
Dativity models a process into three different entities:
- Action
- Data
- Role
The entites relate in the following ways:
- Data (green) is required by an action
- Actions (purple) produce data
- Roles (yellow) perform an action
a simple credit application process
In the above example, the action 'create case' produces the data 'case id' and 'customer-id'. When those pieces of information have been added to the case, 'enter-loan-details' can be performed because it only depends on 'case-id' to be present.
Features
Basic functionality
Given a process definition and a set of collected data, Dativity can answer questions like:
- What actions can be performed next?
- What actions can be performed next by role r
- What actions have been performed?
- Can action a be performed?
- What data is required for action a?
Invalidating
Sometimes a user goes back and modify data. Then all data that is produced 'subsequently' has to be invalidated. Dativity has support for this type of scenario, where the case is 'rewinded' to the action that was re-done. Previously entered data is kept, but 'uncommitted', and depending actions need to be performed again. see example.
Conditional requirements
It is possible to specify that a data is required by an action if and only if a given predicate is true. The condition depends on a specified data. see example.
Examples
Basic functionality
The case data is just a map
(def case {})
Define a case model with actions, data, roles and their relationships.
(def case-model
(dativity.define/create-model
{:actions [:create-case
:enter-loan-details
:produce-credit-application-document
:sign-credit-application-document
:payout-loan]
:data [:case-id
:customer-id
:loan-details
:credit-application-document
:applicant-signature
:officer-signature
:loan-number]
:roles [:applicant
:system
:officer]
:action-produces [[:create-case :customer-id]
[:create-case :case-id]
[:enter-loan-details :loan-details]
[:produce-credit-application-document :credit-application-document]
[:sign-credit-application-document :applicant-signature]
[:sign-credit-application-document :officer-signature]
[:payout-loan :loan-number]]
:action-requires [[:enter-loan-details :case-id]
[:produce-credit-application-document :loan-details]
[:produce-credit-application-document :customer-id]
[:sign-credit-application-document :credit-application-document]
[:payout-loan :applicant-signature]
[:payout-loan :officer-signature]]
:role-performs [[:applicant :create-case]
[:applicant :enter-loan-details]
[:applicant :sign-credit-application-document]
[:officer :sign-credit-application-document]
[:system :payout-loan]
[:system :produce-credit-application-document]]
:action-requires-conditional []}))
Generate an image of the process definition (requires graphviz, clj only).
(dativity.visualize/generate-png case-model)
What actions are possible?
(dativity.core/next-actions case-model case)
=> #{:create-case}
What can the roles do?
(dativity.core/next-actions case-model case :applicant)
=> #{:create-case}
(dativity.core/next-actions case-model case :officer)
=> #{}
What data is produced by ':create-case'?
(dativity.core/data-produced-by-action case-model :create-case)
=> #{:customer-id :case-id}
Add some data to the case to simulate a few actions
(def case
(-> case
(dativity.core/add-data :case-id "542967")
(dativity.core/add-data :customer-id "199209049345")
(dativity.core/add-data :loan-details {:amount 100000 :purpose "home"})))
What actions have been completed?
(dativity.core/actions-performed case-model case)
=> #{:enter-loan-details :create-case}
Who can do what?
(dativity.core/next-actions case-model case :applicant)
=> #{}
(dativity.core/next-actions case-model case :system)
=> #{:produce-credit-application-document}
(dativity.core/next-actions case-model case :officer)
=> #{}
The document is produced and it is signed by the officer
(def case
(-> case
(dativity.core/add-data :credit-application-document {:document-id "abc-123"})))
Who can do what?
(dativity.core/next-actions case-model case :applicant)
=> #{}
(dativity.core/next-actions case-model case :system)
=> #{}
(dativity.core/next-actions case-model case :officer)
=> #{:sign-credit-application-document}
Invalidation
A user might go back in your UI and modify data, it is then likely that 'subsequent' data is no longer valid. For example, if the loan amount is changed, the produced application document is probably not valid anymore.
Dativity supports this via the function invalidate-data. When a data is invalidated, all the data that is produced by actions that depend on the invalidated data is invalidated recursively.
When data is invalidated, it is not deleted. The data is kept, but it is 'uncommitted' which means that dativity will say that actions that depend on the uncommitted data are not allowed.
(def case
(dativity.core/invalidate-data case-model case :loan-details))
Are the loan details gone from the case?
(:loan-details case)
=> {:amount 100000 :purpose "home"}
Now the only available action is to enter loan details again.
(dativity.core/next-actions case-model case :applicant)
=> #{:enter-loan-details}
(dativity.core/next-actions case-model case :system)
=> #{}
(dativity.core/next-actions case-model case :officer)
=> #{}
It's not possible to sign the credit application document.
(dativity.core/action-allowed? case-model case :sign-credit-application-document)
=> false
Conditionally required data
An action-requires-data edge (red arrow in the diagram) can be conditional. The requirement is enforced if and only if a given predicate is true. The predicate is a function of a given data node.
To say that applications for loans of more than 300 000 require signatures from two officers we can write
(def case-model
(dativity.define/add-relationship-to-model case-model
(dativity.define/action-requires-conditional
:payout-loan
:counter-signature
(fn [loan-details]
(> (:amount loan-details) 300000))
:loan-details)))
Dependencies
graphviz
To generate graph images you need graphviz.
Check if it's installed on your system (mac):
dot -v
If not, install it:
brew install graphviz
Ubergraph
Ubergraph is used as an adapter to graphviz for vizualisation and is not used by the main namespaces so it will not be included in a cljs build.
Ysera
Ysera is a convenience library from which testing and errror macros are used.
License
MIT License
Copyright (c) 2019 Morgan Bentell