Skip to content
Jenny Schweers edited this page Nov 11, 2021 · 4 revisions

CommCare Session

CommCare applications are structured around a user beginning at some home state and eventually filling out an XForm. The configuration files define the choices that a user can make in between these points, in the form of static menus the user can choose (commands) from and dynamic lists of other entities (datum).

CommCare keeps track of a user's progress through the application in the form of a Session object. The Session maintains a list of the user's choices, and can be used to determine what the next interactive component should be. Navigating through the application it should be possible to largely define the user's state as a result of this session.

A Session's lifecycle persists until either the user completes a form, and it is processed and saved, or until the user abandons it manually, at which point they will return to the home state and can begin a new session.

Session Stacks

Primer

As of CommCare 2.9, CommCare's behavior has been extended to allow for more control over a user's workflow. When forms are completed, the user can now be directed to different parts of their application, like the form menu for a newly created case or a different form about the same case.

In order to support these workflows a CommCare Session keeps track of a stack of different "frames". Each frame consists of the data associated with proceeding through the app, although only one is live at a time. When a form is completed, the configuration may create one or more frames to push onto the stack before the current frame terminates. When the current frame terminates, if there is a frame on the stack, it is popped from the stack and becomes the current session for the user.

Note on Usage

Directing the user through the application using the session stack should be undertaken carefully and minimally. Consistency is incredibly important in software, and if the user cannot predict where various actions will take them, it can make an application very difficult and unpleasant to use.

It is currently impossible (see data sources below) to build a stack frame directly from a form's contents. One reason for this is that this can create scenarios where the user is unable to recover the state that they ended up in in any other way. Session navigation should help the user by removing redundant navigation, not supplant the users ability to enter data or navigate through the application on their own.

Data Sources

Session operations can only reference information that is global to the application at the time they are executed (after the form is processed). Since form data is only accessible during form entry (and not afterwards), Session Stacks can only be constructed/manipulated based on data that is persisted through cases, or other global models.

Conditional Creation

Session frames can be created or manipulated conditionally. This is helpful when it is only useful to navigate a user to another location if, say, a case was actually created by a registration form.

Example:

  • User selects a create form. One of the datums loaded into the CommCare session is a new random case ID
  • User completes a create form
  • After the form is completed, there is a stack frame creation step. Before it is executed, the system checks to see whether there is a case in the current environment with the ID created. If so, the frame (containing a followup form form) is pushed onto the stack. Otherwise, no frame is pushed.

Similarly, stack frames can be created with an ID. The stack can only contain one frame with a given id at a time, and requests to generate new frames will be ignored. This can be helpful when multiple forms which are possibly part of the same workflow can all end up pushing the same followup onto the stack. Only the first request will actually be pushed, so the user is not mistakenly directed to enter the same form multiple times.

Example:

  • The user is filling out a form, upon completion, two frames are created and pushed to the stack.
  • The first frame has no ID, and is only executed if the case does not have a flag that denotes that it has been triaged.
  • The second frame has an ID (bloodwork), and is executed any time the user has a bloodwork panel followup form entered more than 2 months ago.
  • The first frame is popped and becomes live.
  • The user fills out the triage form. The triage form can be entered at any time, and also tries to create a frame with the ID (bloodwork) based on the same criteria as the first form.
  • Since there is already a bloodwork frame on the stack, the create is ignored.
  • The bloodwork frame is popped and the user enters the form once.
  • Afterwards the stack is empty and the user is directed home.

Stack Lifecycles

The stack is cleared of all session frames if the user navigates home. Additionally, in order to prevent workflows from becoming bewildering, the session stack can be cleared under certain circumstances.

When a frame is popped off the stack and becomes the current session, a snapshot of that frame is saved if there remain more frames on the stack. If at any point a new frame is created, or if the live frame is finishing, the live frame compares its current state to that snapshot. If the two are incompatible (IE: the live frame does not contain all of the steps in order from the snapshot), the stack frame is cleared along with the snapshot.

Example:

  • Registration form is completed -> A demographics form and a referral form for the new case A are placed onto the stack.
  • The demographics form is popped from the stack and becomes the current session. The form is shown to the user. The current frame snapshot is set to the demographics form for case A.
  • The user navigates backwards, and selects a different case B, and fills out their demographics form.
  • Upon completing the form, a new frame is created to be placed on the stack (sending the user to a form select screen for case B. The live frame is compared against its snapshot. Since there is a step in the snapshot (Selecting case A) which is different from the live frame (Selecting case B), the stack is cleared and the snapshot is removed.
  • The stack is now empty. The new frame (containing a form for case B) is placed on it.

Without the stack clearing, the user would be very bewilderingly directed back to the original referral form for the first case.

Mark and Rewind

The 'register from the case list' used in conjunction with advanced modules and the 'case claim' feature require more expressive stack manipulations to be concisely implemented. Instead of manually re-copying stacks we've added the mark and rewind operations

  • Add a new mark frame step. This step marks a point to rewind to. When it is added to a stack frame the current needed datum for that frame is computed and stored with it. Once a rewind targeting a mark is processed, that datum is set with the rewind value.
  • Add a rewind frame step which has a value associated with it. When rewinds are encountered all steps in between it and the nearest mark above it will be cleared and the datum in the mark will be turned into a case_id/SessionFrame.STATE_DATUM_VAL entry.

Here is an example of the syntax:

<stack>
    <push>
        <command value="'m1'"/>
        <mark/>
        <datum id="child_name" value="'steve'"/>
        <rewind value="'billy'"/>
        <!-- results in child_name getting set to 'billy' -->
    </push>
</stack>

Case Claim

Say you have a form entry that requires 2 case ids:

<entry>
  <command id="child-visit-form"/>
  ...
  <session>
    <datum id="mother_id" nodeset="..." value="./@case_id" detail-select="m1_case_short" detail-confirm="m1_case_long"/>
    <datum id="child_id" nodeset="..." value="./@case_id" detail-select="m1_case_short" detail-confirm="m1_case_long"/>

and associated with the m1_case_short case list is the case claim action:

<action>
  <display>
    <text>
      <locale id="case_search.m1"/>
    </text>
  </display>
  <stack>
    <push>
      <mark/>
      <command value="'search_command.m1'"/>
    </push>
  </stack>
</action>

The action, when triggered, runs the search_command.m1 claim process:

<remote-request>
  <post url="https://india.commcarehq.org/..." relevant="...">
     ...
  </post>
  <command id="search_command.m1">
    <display>
      <text>
        <locale id="case_search.m1"/>
      </text>
    </display>
  </command>
  <instance id="querysession" src="jr://instance/session"/>
  <instance id="casedb" src="jr://instance/casedb"/>
  <session>
    <query url="https://india.commcarehq.org/a/enikshay-test/phone/search/" storage-instance="results">
      ...
    </query>
    <datum id="case_id" nodeset="instance('results')/results/case[@case_type='person'][@status='open']" value="./@case_id" detail-select="m1_case_short" detail-confirm="m1_case_long"/>
  </session>
  <stack>
    <push>
      <rewind value="instance('querysession')/session/data/case_id"/>
    </push>
  </stack>
</remote-request>

Now you start your workflow

  • you select a mother case and realize you need to claim the child your stack looks like:
current frame:
  (COMMAND_ID "child-visit-form")
  (CASE_ID mother_id "nancy")
  • you trigger the claim command from the child case list. This will copy the current frame onto the stack and push the claim command onto the current frame:
current frame:
  (COMMAND_ID "child-visit-form")
  (CASE_ID mother_id "nancy")
  (MARK)
  (COMMAND_ID "search_command.m1")
  • you find a case and claim it, this pushes the claimed case onto the current frame as the return value:
current frame:
  (COMMAND_ID "child-visit-form")
  (CASE_ID mother_id "nancy")
  (MARK)
  (COMMAND_ID "search_command.m1")
  (REWIND "billy")

When the step pushes are processed the session logic immediately resolves that frame to

current frame:
  (COMMAND_ID "child-visit-form")
  (CASE_ID mother_id "nancy")
rewind value: "billy"
  • The session then sees what the next needed datum is for the child-visit-form command, which is child_id. It notices that there is a rewind value set, and uses that for the child_id
  • 🎉 🎈

Double Management

and associated with the m1_case_short case list is the case claim action:

<action>
  <display>
    <text>
      <locale id="case_list_form.m1"/>
    </text>
  </display>
  <stack>
    <push>
      <mark/>
      <command value="'child-registration-form'"/>
      <datum id="case_id_new_case_0" value="uuid()"/>
      <datum id="child_double_management" value="true()"/>
    </push>
  </stack>
</action>

and when you trigger the action you are taken to the child-registration-form, with the following entry:

<entry>
  <form>http://openrosa.org/formdesigner/11FAC65A-F2CD-427F-A870-CF126336AAB5</form>
  <command id="child-registration-form">
    <text>
      <locale id="forms.m0f0"/>
    </text>
  </command>
  <instance id="casedb" src="jr://instance/casedb"/>
  <instance id="commcaresession" src="jr://instance/session"/>
  <session>
    <datum id="case_id_new_case_0" function="uuid()"/>
  </session>
  <stack>
    <push if="count(instance('commcaresession')/session/data/person_double_management) = 1 and 
              count(instance('casedb')/casedb/case[@case_id=instance('commcaresession')/session/data/case_id_new_case_0]) &gt; 0">
      <rewind value="instance('commcaresession')/session/data/case_id_new_case_0"/>
    </push>
    <!-- normal, non-double management form linking can occur here -->
    <!-- NOTE: these stack operations will be skipped if a rewind occurs -->
  </stack>
</entry>

Other Implementation Details:

  • A rewind that doesn't have an associated mark is skipped over
  • If a rewind is processed, subsequent stack frame steps and operations will be disregarded.
  • mark/rewinds can occur in create stack operations but will just get simplified out at the time that the create is processed.
  • rewinds will find the nearest mark. Hence, you can have more than one more mark, in the case that you want to rewind more than once.

Jump

The jump stack directive notifies CommCare that the user should be redirected to a specific URL. The URL is presumed to be a smart link into another CommCare application, although this is not currently enforced.

Jumps upend navigation, so if one is present in a stack frame, a platform shouldn’t expect to have any navigation steps afterwards. On a browser, this means that a jump should be implemented as a redirect rather than, say, loading a separate tab.

Jump directives have a single url child element specifying the destination. The url element contains a element whose output is the destination. This provides a convenient mechanism for templating arguments into the url:

<jump>
  <url>
    <text>
      <xpath function="concat('http://india.commcarehq.org/a/', $domain, '/app/v1/123/followup/', '?case_id=', $case_id)">
        <variable name="domain">
          <xpath function="instance('casedb')/casedb/case[@case_id=instance('commcaresession')/session/data/case_id]/commcare_project"/>
        </variable>
        <variable name="case_id">
          <xpath function="instance('commcaresession')/session/data/case_id"/>
        </variable>
      </xpath>
    </text>
  </url>
</jump>