Writing rules

Rules are just normal JavaScript functions that take some input and returns something. You can use the full power of JavaScript in these functions. We also provide you with some helper libraries that make it easier to write rules. We will introduce you to these libraries in the examples below.

All rule functions get passed an object as a parameter. The parameter object has two properties: 1. imports 2. params. The imports object is used to pass down common libraries. The params object is used to pass rule-specific parameters. In params object, we pass the relevant entity e.g. if a rule is invoked when a program encounter is being performed then we pass the ProgramEncounter object. The entities that we pass are an instance of classes defined in avni-models

Shape of common imports object:

{
  rulesConfig: {}, //It exposes everything exported by rules-config library. https://github.com/avniproject/rules-config/blob/master/rules.js.
  common: {}, // Library we have for common functions https://github.com/avniproject/avni-client/blob/master/packages/openchs-health-modules/health_modules/common.js
  models: {} // It exposes everything exported by avni-models library
  lodash: {} // lodash library
  moment: {} // momentjs library
}

Entities passed to the rule

All rule receives an entity from the params object. Depending on the rule type an entity can be one of Individual, ProgramEncounter, ProgramEnrolment, Encounter, or ChecklistItem. The shape of the entity object and the supported methods can be viewed from the above links on each entity.

Rule types

  1. Enrolment summary rule
  2. Form element rule
  3. Form element group rule
  4. Visit schedule rule
  5. Decision rule
  6. Validation rule
  7. Enrolment eligibility check rule
  8. Encounter eligibility check rule
  9. Checklists rule
  10. Work list updation rule
  11. Subject summary rule

Invocation of different rule typesInvocation of different rule types

Invocation of different rule types


Invocation of rules affecting the form elementsInvocation of rules affecting the form elements

Invocation of rules affecting the form elements



1. Enrolment summary rule

  • Logical scope = Program Enrolment
  • Trigger = Before the opening of a subject dashboard with default program selection. On program change of subject dashboard.
  • In designer = Program (Enrolment Summary Rule)
  • When to use = Display important information in the subject dashboard for a program

You can use this rule to highlight important information about the program on the Subject Dashboard in table format. It can pull data from all the encounters of enrolment and the enrolment itself. You can use this when the information you want to show is not entered by the user in any of the forms and is also not required for any reporting purposes (hence you wouldn't also generate this data via decision rule).

Shape of params object:

{ 
  programEnrolment: {}, // ProgramEnrolment model
}

You need to return an array of summary objects from this function.

Shape of the summary object:

{
  "name": "name of the summary concept",
  "value": <text> | <number> | <date> | <datetime> | <concept list in case of Coded question>
}

Example:

({params, imports}) =>  {
    const summaries = [];
    const programEnrolment = params.programEnrolment;
    const birthWeight = programEnrolment.findObservationInEntireEnrolment('Birth Weight');
    if (birthWeight) {
      summaries.push({name: 'Birth Weight', value: birthWeight.getValue()});
    }
    return summaries;
};
Enrolment summary rule in App DesignerEnrolment summary rule in App Designer

Enrolment summary rule in App Designer


Summary section displays the data returned by the enrolment summary ruleSummary section displays the data returned by the enrolment summary rule

Summary section displays the data returned by the enrolment summary rule



2. Form element rule

  • Logical scope = Form Element
  • Trigger = Before display of form element in the form wizard and on any change done by the user in on that page
  • In designer = Form Element (RULES tab)
  • When to use = Hide/show a form element, auto calculate the value of a form element

Shape of params object:

{
  entity: {}, //it could be one of Individual, ProgramEncounter, ProgramEnrolment, Encounter and ChecklistItem depending on what type of form is this rule attached to
  formElement: {}, //form element to which this rule is attached to
}

This function should return an instance of FormElementStatus to show/hide the element, show validation error, set its value, or skip answers.
You can either use FormElementStatusBuilder or use normal JavaScript to build the return value. FormElementStatusBuilder is a helper class provided by Avni that helps writing rules in a declarative way.

Examples using FormElementStatusBuilder.

'use strict';
({params, imports}) => {
  const individual = params.entity;
  const formElement = params.formElement;
  const statusBuilder = new imports.rulesConfig.FormElementStatusBuilder({individual, formElement});
  statusBuilder.show().when.valueInRegistration("Number of hywas required").is.greaterThan(0);
  return statusBuilder.build();
};
({params, imports}) => {
  const programEnrolment = params.entity;
  const formElement = params.formElement;
  const statusBuilder = new imports.rulesConfig.FormElementStatusBuilder({programEnrolment, formElement});
  statusBuilder.show().when.valueInEnrolment('Is child getting registered at Birth').containsAnswerConceptName("No");
  return statusBuilder.build();//this method returns FormElementStatus object with visibility true if the conditions given above matches
};
({params, imports}) => {
    const gravidaBreakup = [
        'Number of miscarriages',
        'Number of abortions',
        'Number of stillbirths',
        'Number of child deaths',
        'Number of living children'
    ];
    const computeGravida = (programEnrolment) => gravidaBreakup
        .map((cn) => programEnrolment.getObservationValue(cn))
        .filter(Number.isFinite)
        .reduce((a, b) => a + b, 1);
    
    const [formElement, programEnrolment] = params.programEnrolment;
    const firstPregnancy = programEnrolment.getObservationReadableValue('Is this your first pregnancy?');
    const value = firstPregnancy === 'Yes' ? 1 : firstPregnancy === 'No' ? computeGravida(programEnrolment) : undefined;
    return new FormElementStatus(formElement.uuid, true, value);
};
'use strict';
({params, imports}) => {
  const programEncounter = params.entity;
  const formElement = params.formElement;
  const statusBuilder = new imports.rulesConfig.FormElementStatusBuilder({programEncounter, formElement});
  const value = programEncounter.findLatestObservationInEntireEnrolment('Have you received first dose of TT');
  statusBuilder.show().whenItem( value.getReadableValue() == 'No').is.truthy;
  return statusBuilder.build();
};
'use strict';
({params, imports}) => {
  const encounter = params.entity;
  const formElement = params.formElement;
  const statusBuilder = new imports.rulesConfig.FormElementStatusBuilder({encounter, formElement});
  statusBuilder.show().when.valueInEncounter("Are machine start and end hour readings recorded").is.yes;
  return statusBuilder.build();
};

Skip logic in action for the field userSkip logic in action for the field user

Skip logic in action for the field user



3. Form element group rule

  • Scope = Form Element Group
  • Trigger = Before display of form element group to the user (including previous or next)
  • In designer = Form Element Group (RULES tab)
  • When to use = Hide/show a form element group

Sometimes we want to hide the entire form element group based on some conditions. This can be done using a form element group (FEG) rule. There is a rules tab on each FEG where this type of rule can be written. Note that this rule gets executed before form element rule so if the form element is hidden by this rule then the form element rule will not get executed.

Shape of params object:

{
  entity: {}, //it could be one of Individual, ProgramEncounter, ProgramEnrolment, Encounter and ChecklistItem depending on what type of form is this rule attached to
  formElementGroup: {}, //form element group to which this rule is attached to
}

This function should return an array of FormElementStatus

Example:

({params, imports}) => {
    const formElementGroup = params.formElementGroup;
    return formElementGroup.formElements.map(({uuid}) => {
        return new imports.rulesConfig.FormElementStatus(uuid, false, null);
    });
};



4. Visit schedule rule

  • Logical scope = Encounter (aka Visit), Subject, or Program Enrolment
  • Trigger = On completion of an form wizard before final screen is displayed
  • In designer = Form (RULES tab)
  • When to use = For scheduling one or more encounters in the future

Shape of params object:

{
  entity: {}, //it could be one of ProgramEncounter, ProgramEnrolment, Encounter depending on what type of form is this rule attached to.
  visitSchedules: []// Array of already scheduled visits. 
}

You need to return an array of visit schedules from this function.

Shape of the return value

[
  <visit schedule object>
  ...
]

visit schedule object

{
    name: "visit name", 
    encounterType: "encounter type name", 
    earliestDate: <date>, 
    maxDate: <date>,
    visitCreationStrategy: "Optional. One of default|createNew",
    programEnrolment: "<Optional. Used if you want to create a visit in a different program enrolment. If the program enrolment is tied to another subject, the visit will be schedule for that subject. Do not pass this parameter if you want to schedule a general encounter.>",
    subjectUUID: "<Optional UUID string. Used if you want to create a general visit for another subject.>"
}

Example

({ params, imports }) => {
  const programEnrolment = params.entity;
  const scheduleBuilder = new imports.rulesConfig.VisitScheduleBuilder({
    programEnrolment
  });
  scheduleBuilder
    .add({
      name: "First Birth Registration Visit",
      encounterType: "Birth Registration",
      earliestDate: programEnrolment.enrolmentDateTime,
      maxDate: programEnrolment.enrolmentDateTime
    })
    .whenItem(programEnrolment.getEncounters(true).length)
    .equals(0);
  return scheduleBuilder.getAll();
};

Example 2 - Schedule a general visit on a household when a member completes a program enrolment

.
.
  scheduleBuilder.add({
      name: "TB Family Screening Form",
      encounterType: "TB Family Screening Form",
      earliestDate: imports.moment(programEnrolment.encounterDateTime).toDate(),
      maxDate: imports.moment(programEnrolment.encounterDateTime).add(15, 'days').toDate(),
      subjectUUID: programEnrolment.individual.groups[0].groupSubject.uuid
  });
.
.
Screenshot - App DesignerScreenshot - App Designer

Screenshot - App Designer


Returned encounters/visits from function are shown to the user as - Visits Being ScheduledReturned encounters/visits from function are shown to the user as - Visits Being Scheduled

Returned encounters/visits from function are shown to the user as - Visits Being Scheduled

Strategies that Avni uses.

For all the visit schedules that are returned, Avni evaluates how to create a visit. Assume you provide the default visitCreationStrategy (this is the default behaviour). Avni checks if there is already a scheduled visit for the given encounter type. If it is there, then it is updated with the incoming scheduled visit's name and other parameters. This strategy works well in most cases.

  • Remember that the VisitSchedule rule gets called whether you create a visit, or edit it.
  • Remember not to send multiple visit schedule objects for the same encounter type. If you do, the last one will overwrite the previous objects.

Using the "createNew" visit strategy

Do this only if you know what you are doing. If you add visitCreationStrategy of "createNew", then a new visit will be created no matter what.

You need to be careful while using this strategy because, in edit scenarios, we might end up creating the same kind of visits multiple times.

Using the VisitScheduleBuilder.getAllUniqueVisits

VisitSchedulBuilder class has a getAllUniqueVisits method that provides some shortcuts to reduce the cruft you might have to do while creating scheduled visits. It mostly does the right thing, so you don't have to worry about its logic. However, if you think it is doing something you didn't intend, then you can replace it with your own implementation. Look up the code for more details.



5. Decision rule

  • Logical scope = Encounter (aka Visit), Subject, or Program Enrolment
  • Trigger = On completion of an form wizard before final screen is displayed
  • In designer = Form (RULES tab)
  • When to use = To create any additional observations based on all the data filled by the user in the form

Used to add decisions/recommendations to the form. The decisions are displayed on the last page of the form and are also saved in the form's observations.

Shape of params object:

{
  entity: {}, //it could be ProgramEncounter, ProgramEnrolment or Encounter depending on what type of form is this rule attached to.
  decisions: []// Decisions object on which you need to add decisions. 
}

Shape of decisions parameter:

{
  "enrolmentDecisions": [],
  "encounterDecisions": [],
  "registrationDecisions": []
}

You need to add to decisions parameter's appropriate field and return it back.
Inside the function, you will build decisions using ComplicationsBuilder and push the decisions to the decisions parameter's appropriate field. The return value will be the modified decisions parameter. You can also choose to not use ComplicationsBuilder and directly construct the return value as per the contract shown below:

Shape of the return value

{
  "enrolmentDecisions": [<decision object>, ...],
  "encounterDecisions": [<decision object>, ...],
  "registrationDecisions": [<decision object>, ...]
}
The shape of <decision object>
{
  "name": "name of the decision concept",
  "value": <text> | <number> | <date> | <datetime> | <name of anwer concepts in case of Coded question>
}

Example

({params, imports}) => {
    const programEncounter = params.entity;
    const decisions = params.decisions;
    const complicationsBuilder = new imports.rulesConfig.complicationsBuilder({
        programEncounter: programEncounter,
        complicationsConcept: "Birth status"
    });
    complicationsBuilder
        .addComplication("Baby is over weight")
        .when.valueInEncounter("Birth Weight")
        .is.greaterThanOrEqualTo(8);
    complicationsBuilder
        .addComplication("Baby is under weight")
        .when.valueInEncounter("Birth Weight")
        .is.lessThanOrEqualTo(5);
    complicationsBuilder
        .addComplication("Baby is normal")
        .when.valueInEncounter("Birth Weight")
        .is.lessThan(8)
        .and.when.valueInEncounter("Birth Weight")
        .is.greaterThan(5);
    decisions.encounterDecisions.push(complicationsBuilder.getComplications());
    return decisions;
};

Decision rule output are displayed in system recommendations section.Decision rule output are displayed in system recommendations section.

Decision rule output are displayed in system recommendations section.



6. Validation rule

  • Logical scope = Encounter (aka Visit), Subject, or Program Enrolment
  • Trigger = On completion of an form wizard before final screen is displayed
  • In designer = Form (RULES tab)
  • When to use = To provide validation error(s) to the user that are not specific to one form element but involved data in multiple form elements.

Used to stop users from filling invalid data

Shape of params object:

{
  entity: {}, //it could be ProgramEncounter, ProgramEnrolment or Encounter depending on what type of form is this rule attached to.
}

The return value of this function is an array with validation errors.

Example:

({params, imports}) => {
  const validationResults = [];
  if(programEncounter.getObservationReadableValue('Parity') > programEncounter.getObservationReadableValue('Gravida')) {
    validationResults.push(imports.common.createValidationError('Para Cannot be greater than Gravida'));
  }
  return validationResults;
};



7. Enrolment Eligibility Check Rule

  • Logical scope = Subject
  • Trigger = On launch of program list when user enrols a subject into program
  • In designer = Program page
  • When to use = To restrict the programs which are available for enrolment based on subject's data (e.g. not allowing males to enrol in pregnancy programs)

Shape of params object:

{
  entity: {}//Subject will be passed here.
}

Shape of the return value

The return value of this function should be a boolean.

Example:

({params, imports}) => {
  const individual = params.entity;
  return individual.isFemale() && individual.getAgeInYears() > 5;
};

Notes: The eligibility check is triggered only when someone tries to create a visit manually. Form stitching rules can override this default behaviour.


This list can be controlled by this rule.This list can be controlled by this rule.

This list can be controlled by this rule.



8. Encounter Eligibility Check Rule

  • Logical scope = Subject or Program Enrolment
  • Trigger = On launch of new visit (encounter) list
  • In designer = Encounter page
  • When to use = To restrict the encounters which are available based on subject's full data (e.g. not showing postnatal care form if the delivery form has not been filed yet)

Used to hide some visit types depending on some data

Shape of params object:

{
  entity: {}//Subject will be passed here.
}

Shape of the return value

The return value of this function should be a boolean.

Example:

({params, imports}) => {
  const individual = params.entity;
  const visitCount = individual.enrolments[0].encounters.filter(e => e.encounterType.uuid === 'a30afe96-cdbb-42d9-bf30-6cf4b07354d1').length;
  let visibility = true;
  if (_.isEqual(visitCount, 1)) visibility = false;
  return visibility;
};

Notes: The eligibility check is triggered only when someone tries to create a visit manually. Form stitching rules can override this default behaviour.



9. Checklists rule

Used to add a checklist to an enrolment

Shape of params object:

{
  "entity": {} //ProgramEnrolment
  "checklistDetails": [] // Array of ChecklistDetail
}

Example

({params, imports}) => {
  let vaccination = params.checklistDetails.find(cd => cd.name === 'Vaccination');
  if (vaccination === undefined) return [];
  const vaccinationList = {
    baseDate: params.entity.individual.dateOfBirth,
    detail: {uuid: vaccination.uuid},
    items: vaccination.items.map(vi => ({
      detail: {uuid: vi.uuid}
    }))
  };
  return [vaccinationList];
};



10. Work List Updation rule

  • Logical scope = Subject, Program Enrolment, or Encounters
  • Trigger = On display of system recommendation's page in form wizard
  • In designer = Main Menu
  • When to use = Stitch together multiple forms which can be filled back to back

The System Recommendations screen of Avni can be configured to direct a user to go to the next task to be done. Typically, if a new encounter is scheduled for a person on the same day, then the system automatically prompts the user to perform that encounter.
This is performed using worklists. A worklist is an array of work items.

The WorkListUpdation rule is used to customize this flow. The WorkLists object is passed on to this rule just before showing the System Recommendations screen. Any modification in the worklists is applied immediately to the flow.

You can add a new WorkItem anywhere after the currentWorkList.currentItem.

Shape of params object:

{
  worklists: {},
  context: {}
}

Example

https://gist.github.com/hithacker/d0fe89107b974797fbb11ced1feda146



11. Subject summary rule

  • Logical scope = Subject registration
  • Trigger = Before the opening of the subject dashboard profile tab.
  • In designer = Subject (Subject Summary Rule)
  • When to use = Display important information in the subject's profile. It can be used to show the summary if there are no programs.

This rule is very similar to the Enrolment summary rule. Except its scope is the Subject's registration.

Shape of params object:

{ 
  individual: {}, // Subject model
}

You need to return an array of summary objects from this function.

Shape of the summary object:

{
  "name": "name of the summary concept",
  "value": <text> | <number> | <date> | <datetime> | <concept list in case of Coded question>
}

Example:

({params, imports}) =>  {
    const summaries = [];
    const individual = params.individual;
    const mobileNumber = individual.findObservation('Mobile Number'); 
    if(mobileNumber) {
      summaries.push({name: 'Mobile Number', value: mobileNumber.getValueWrapper()});
    }
    return summaries;
};



Using service methods in the rules

Often, there is the need to get the context of implementation beyond what the models themselves provide. For example, the knowledge of other subjects in the location might be necessary to run a specific rule. For such scenarios, Avni provides querying the DB using the services passed to the rules.

The services object looks like this

{
    individualService: '',
}

Right now only individual service is injected into all the rules. One method which is implemented right now returns an array of subjects in a particular location. The method looks like the following, it takes address-level object and subject type name as its parameters and returns a list of all the subjects in that location.

getSubjectsInLocation(addressLevel, subjectTypeName) {
  const allSubjects = ....;
  return allSubjects;
}

Note that this function is not implemented for the data entry app and throws a "method not supported" error for all the rules when run from the data entry app.

Example

The view-filter rule is for the subject data type concept that displays all the subjects of type 'Person' in the passed location.

'use strict';
({params, imports}) => {
  const encounter = params.entity;
  const formElement = params.formElement;
  const statusBuilder = new imports.rulesConfig.FormElementStatusBuilder({encounter, formElement});
  const individualService = params.services.individualService;
  const subjects = individualService.getSubjectsInLocation(encounter.individual.lowestAddressLevel, 'Person');
  const uuids = _.map(subjects, ({uuid}) => uuid);
  statusBuilder.showAnswers(...uuids);
  return statusBuilder.build();
};

What’s Next