How to implement skip logic in LinkML

This guide will show you how to implement skip logic in your LinkML schemas. Skip logic, also known as conditional branching or survey routing, is a feature that allows you to control which questions are shown to a user based on their previous answers. This is a common requirement in systems like REDCap, CEDAR, and other questionnaire-based data collection systems.

What is Skip Logic?

In a questionnaire, some questions may only be relevant if a specific answer was given to a previous question. For example, a question about the due date of a pregnancy is only relevant if the respondent has indicated that they are pregnant. Skip logic allows you to hide or show questions based on these conditions, creating a more dynamic and user-friendly experience.

Modeling Questionnaires in LinkML

You can model a questionnaire in LinkML by defining a class for the questionnaire itself, and then adding slots for each question. For example:

id: https://w3id.org/linkml/examples/personinfo
name: personinfo
prefixes:
  linkml: https://w3id.org/linkml/
  personinfo: https://w3id.org/linkml/examples/personinfo/
default_prefix: personinfo

imports:
  - linkml:types

classes:
  Person:
    slots:
      - is_pregnant
      - due_date
      - has_children
      - number_of_children

In this schema, we have a Person class with four slots representing questions in a questionnaire.

Implementing Skip Logic with LinkML Rules

LinkML’s rules feature can be used to implement skip logic. Rules are composed of preconditions and postconditions. The preconditions define the conditions under which the rule applies, and the postconditions define the constraints that should be enforced when the preconditions are met.

Pattern 1: Making a field inapplicable

In some cases, a question may not be applicable based on a previous answer. For example, if a person is not pregnant, the due_date question is not applicable. We can model this by making the due_date slot inapplicable if the is_pregnant slot is false.

classes:
  Person:
    slots:
      - is_pregnant
      - due_date
      - has_children
      - number_of_children
    rules:
      - description: "If a person is not pregnant, the due date is not applicable."
        preconditions:
          slot_conditions:
            is_pregnant:
              equals_string: "no"
        postconditions:
          slot_conditions:
            due_date:
              inapplicable: true

With this rule, if a user enters “no” for is_pregnant, any value entered for due_date will be considered a validation error. The data validation engine would expect the due_date to be null or absent.

Pattern 2: Making a field conditionally required

Another common pattern is to make a field required only if a certain condition is met. For example, the due_date should be required if the person is pregnant.

classes:
  Person:
    slots:
      - is_pregnant
      - due_date
      - has_children
      - number_of_children
    rules:
      - description: "If a person is pregnant, the due date is required."
        preconditions:
          slot_conditions:
            is_pregnant:
              equals_string: "yes"
        postconditions:
          slot_conditions:
            due_date:
              required: true

In this case, if is_pregnant is “yes”, the due_date slot must have a value.

Combining Rules

You can combine these rules to create more complex logic. For example, we can combine the two rules above, and add similar logic for the number_of_children slot.

id: https://w3id.org/linkml/examples/personinfo
name: personinfo
prefixes:
  linkml: https://w3id.org/linkml/
  personinfo: https://w3id.org/linkml/examples/personinfo/
default_prefix: personinfo

imports:
  - linkml:types

enums:
  YesNoEnum:
    permissible_values:
      "yes":
      "no":

slots:
  is_pregnant:
    range: YesNoEnum
    description: "Are you pregnant?"
  due_date:
    range: date
    description: "What is your due date?"
  has_children:
    range: YesNoEnum
    description: "Do you have children?"
  number_of_children:
    range: integer
    description: "How many children do you have?"

classes:
  Person:
    slots:
      - is_pregnant
      - due_date
      - has_children
      - number_of_children
    rules:
      - description: "Due date is required if pregnant, otherwise it's inapplicable."
        preconditions:
          slot_conditions:
            is_pregnant:
              equals_enum:
                codeable_concept: "yes"
        postconditions:
          slot_conditions:
            due_date:
              required: true
      - preconditions:
          slot_conditions:
            is_pregnant:
              equals_enum:
                codeable_concept: "no"
        postconditions:
          slot_conditions:
            due_date:
              inapplicable: true

      - description: "Number of children is required if has_children is yes, otherwise it's inapplicable."
        preconditions:
          slot_conditions:
            has_children:
              equals_enum:
                codeable_concept: "yes"
        postconditions:
          slot_conditions:
            number_of_children:
              required: true
      - preconditions:
          slot_conditions:
            has_children:
              equals_enum:
                codeable_concept: "no"
        postconditions:
          slot_conditions:
            number_of_children:
              inapplicable: true

This complete example demonstrates how to use LinkML rules to implement skip logic in your schemas, ensuring that your data is valid and consistent. By using these patterns, you can create sophisticated and dynamic questionnaires that adapt to user input.