Light Business Process Management (BPM) in Nodejs / Typescript

I was trying to convince my co-workers to use Camunda platform for orchestrating business processes in a project, however they are not ready yet to add another component in the stack. So, I created a small light weight business process library to manage processes that can be embedded in your nodejs / typescript code.

I designed the sample process using Camunda Modeler for onboarding customers and kyc (know your customer) process.

Let's imagine that we are in the business of agriculture and we had a land of 100k acres and we rent these lands for small farmers, let's call the great-micro-farmers-network

In order for a farmer to rent this land, they need to register with us in our online portal. This onboarding process includes these steps: 1- Farmer should fill a form and create an account based on his email 2- He then should activate his account by entering 6 digits received in his email 3- Then the farmer should show how much capital he wants to investment and how much acres he is willing to rent, let's call it investment form 4- We also ask him about his experience in farming, we can call this experience form 5- We also ask him if he needs tools and equipment's, and when he will rent the land, and which crops he is interested farming, the needs form 6- Then we want him to upload an ID and a Photo 7- Finally his account has to be approved and verified by administrator

So these are the 7 steps needs to be executed in order to successfully board the customer in our system.Using Camunda Modeler, we can design our business process as follows:

farmer-account-registration.png

Of course Camunda is powerful and we could simply deploy the process but this might be covered in another tutorial. For now, I want to create a reusable codebase to manage the state of a process (until my coworkers agree on using Camunda).

So, let's imagine that we have a token, this token will move across the tasks in a defined order. The first 4 tasks has to be completed sequentially, and the state should be stored in the database. That is if the client refreshed the browser he should continue where he left.

On task 6, the client has two tasks that can be done in parallel, he can either upload his Identity first, or he can upload his photo. That is the (+) sign in the Camunda model. Once both tasks are completed, then the admin can approve the account and then a welcome email will be sent to the customer.

so my idea is to create a BusinessProcess class. the class will keep a list of open tasks needs to be finished, which is basically and array of strings. Once a task is completed, new tasks are added to the process respecting the order, this way we can track current and completed tasks in the process.

export BusinessProcess {
    currentTasks: string[] = [];
    completedTasks: string[] = [];
}

We also need to define initial tasks (when starting the process). Also we define a list of available tasks. These two properties are defined in the constructor

export BusinessProcess {
    currentTasks: string[] = [];
    completedTasks: string[] = [];
    constructor(
        readonly inititalTasks: string[],
        readonly availableTasks: BusinessTask[]
    ) { }
}

We notice that availableTasks is of type BusinessTask. The BusinessTask is a class contains properties like taskName, taskType (UserTask | SystemTask) and a function called nextTasks. The function receives the instance of a process and return a list of next task names. This list will be added to process when a task is completed. The BusinessTask is defined as follows:

export enum TaskType {
    UserTask = 'UserTask',
    ServiceTask = 'ServiceTask'
}

export class BusinessTask {
    constructor(
        readonly taskName: string,
        readonly taskType: TaskType,
        readonly nextTasks: (process: BusinessProcess) => string[],
    ) { }
}

Next we define two methods in BusinessProcess class: ther start() and complete(taskName: string) methods. The start function is used to start the process which basically copies the initalTask names into the currentTasks of the process:

start() {
        this.currentTasks = [...this.inititalTasks];
 }

The complete task will receives a taskName, checks it active in currentTask array, remove the task (thus completed) then the nextTasks to the process.

complete(taskName: string) {
        // ensure task is currently active
        if (!this.currentTasks.includes(taskName))
            throw new CommandError(`Task "${taskName}" not in current active tasks of the process`, 'TASK_NOT_ACTIVE');

        // get business task
        const foundBusinessTask = this.availableTasks.find(t => t.taskName === taskName);
        if (!foundBusinessTask)
            throw new CommandError(`Task "${taskName}" not available in process`, 'TASK_NOT_AVAILABLE');

        // check task constraint
        const ok = foundBusinessTask.completeConstraint(this);
        if (!ok) throw new CommandError(`Task "${taskName}" transition contraint failed`, 'TASK_CONSTRAINT_FAILED');

        // update process
        this.currentTasks.push(...foundBusinessTask.nextTasks(this));
        this.currentTasks = this.currentTasks.filter(t => t != taskName);
        this.completedTasks.push(taskName);
 }

In next section, we will show how to create a process for the registration of the farmer

creating the process

The below code allows us to create the farmer boarding business process:

createProcess() {
        return new BusinessProcess([
            'start'
        ], [
            // task 1
            new BusinessTask('start', TaskType.UserTask, 
                () => ['user.fillInvestmentForm']),
            // task 2
            new BusinessTask('user.fillInvestmentForm', TaskType.UserTask,
                () => ['user.fillExperianceForm']),
            new BusinessTask('user.fillExperianceForm', TaskType.UserTask, 
                () => ['user.fillNeedsForm',]),
            new BusinessTask('user.fillNeedsForm', TaskType.UserTask,
                () => [
                             'user.uploadIdentity',
                             'user.uploadPhoto',
                 ]),
            new BusinessTask('user.uploadIdentity', TaskType.UserTask,
                () => ['admin.approveAccount']),
            new BusinessTask('user.uploadPhoto', TaskType.UserTask, 
                () => ['admin.approveAccount'], 
            new BusinessTask('admin.approveAccount', TaskType.UserTask,
                () => ['system.sendWelcomeEmail']),
            new BusinessTask('system.sendWelcomeEmail', TaskType.UserTask,
                () => []),
        ])
    }

To use the process, follow this way:

const registrationProcess = createProcess();

registrationProcess.start();
console.log(registrationProcess.currentTasks); // ['user.fillInvestmentForm']

registrationProcess.complete('user.fillInvestmentForm');
console.log(registrationProcess.currentTasks); // ['user.fillExperianceForm']

registrationProcess.complete('user.fillExperianceForm');
console.log(registrationProcess.currentTasks); // ['user.fillNeedsForm']

registrationProcess.complete('someRandomTaskName'); // throw taskName not available error
registrationProcess.complete('admin.approveAccount'); // throw taskName not active error

registrationProcess.complete('user.fillNeedsForm'); // ok
registrationProcess.complete('user.uploadPhoto'); // ok
registrationProcess.complete('user.uploadIdentity'); // ok
registrationProcess.complete('user.approveAccount'); // ok registrationProcess.complete('system.sendWelcomeEmail'); // ok, no more active tasks

And that's it, you have made it to the end.

This has been published as npm package @h-platform/bpm. It is MIT license. You can try it and your feedback are welcome.