In Part 2, we built the structural foundation for the Priority Score form on the Change Request page. The jsonObject field, the link type, the relationship component, the groupable container, the zero state and assessment layouts, the Embedded Builder Page field, and the read-only Priority Score dropdown are all in place. Previewing the page at the end of Part 2 showed exactly one thing — that read-only dropdown sitting alone — because none of the pieces know how to talk to each other yet.
In this post, we will write the interactions that bring the form to life: load it onto the page, capture answers as the user fills them in, write the calculated priority back to the Change Request, and make sure the form reappears every time the record is reopened.
Think of it like attaching a paper questionnaire to a manila folder. The user picks a questionnaire, fills it out, and the result appears on the front of the folder so anyone walking by can see it without opening it. There are only two quirks worth knowing up front. First, the form runs inside a sandbox — it can calculate a result, but it cannot reach out and write to fields on the Change Request directly. Second, that sandbox has no memory, so every time the page opens, the form has to be redrawn from scratch. Everything that follows is plumbing to handle those two facts.
We will create four new interactions on the Change Request page:
We will also add one line to the form itself so it actually sends its result out, and we will override the four out-of-the-box page load handlers so the form rehydrates on every open. The Approval Checklist on the same Change Request page is a working reference for all of this, so when in doubt, open those pages and compare.
Here is the whole job at a glance so you always know where you are:
| # | What you set up | Why |
| 1 | How the page receives the priority from the form | Without this, the form calculates a priority but the Change Request never gets one |
| 2 | How the form sends the priority back | Without this, the priority stays trapped inside the form |
| 3 | How the page wakes the form up | The form forgets itself every time the page opens; This is what brings it back |
| 4 | What happens on attach and detach | Picking a form should show it; removing one should clean up |
| 5 | The form reappears every time the page opens | Otherwise the form works on day one and looks broken on day two |
Setting up the receiver on the Change Request page
In plain terms, we are telling the page: "when the form sends you a priority value, write it onto the Change Request."
The Embedded Builder Page component has exactly one item action: onTriggerParent. This is the only channel the form has for sending values back to the parent. Whatever interaction we bind to this slot will fire every time the form calls triggerParent, with the form's payload available as local.input_trigger.
Open the Change Request page in Cirrus Builder.
Click the Embedded Builder Page component, x_priorityQuestions, inside the assessment_layout container.
In the Properties menu, find the Interactions section and the Item actions subsection. On the onTriggerParent slot, click Add interaction and name it x_setPriorityAssessmentScore.
This interaction has one job: take the value the form sent up and write it to the Priority Score field on the Change Request.
#This interaction takes the input trigger value from the embedded builder page # (the linked assessment form) and uses it to set the Priority Score assessment result
field.x_cr_dd_priorityScore = local.input_trigger;
Click Save. The page now knows how to handle a priority value when one arrives. Nothing is arriving yet.
Setting up the sender on the form side
The receiver is in place but nothing is calling it yet. The form already knows how to calculate the priority score. That logic lives in the x_calculatePriorityScore interaction we built in Part 1. At this state, the logic stops at computing the result locally. To close the loop, we need the form to push that result out through triggerParent.
Navigate to the SAS Model Risk Management user interface and open the IT Change Priority Assessment form. Open the x_calculatePriorityScore interaction.
At the bottom of the existing logic, add the following two lines:
#─── Step 5: Write the output to the triggerParent ────────────────────────── global.setTrigger = local.Result; CirrusLibPageData.triggerParent(global.setTrigger);
Click Save on the interaction, close the editor, and click Save on the form.
Now, every time the user changes an answer in the form, the calculated priority result is passed through triggerParent. The Embedded Builder Page on the Change Request page receives it on its onTriggerParent slot, which fires x_setPriorityAssessmentScore, which writes the value to the Priority Score field in real time.
x_loadPriorityAssessmentData
The form will not render on its own. Every time the Change Request page opens, the EBP starts blank, and something has to fetch the linked form's template, draw it on the screen, and rehydrate any saved answers from the jsonObject field. That is what this interaction does.
Rather than building this from scratch, we will copy the Approval Checklist version. The mrm_loadApprovalChecklistData interaction is designed to be reusable. You can see that by the object-specific values declared as local variables at the top, and the rest of the logic does not need to change between objects.
In the Interactions Editor, find mrm_loadApprovalChecklistData. Copy it. Rename the copy to x_loadPriorityAssessmentData.
Open it in the Script Editor.
At the top, update the three local variables:
local.linkedFormField = "x_mrm_linkedPriorityForm";
local.embeddedBuilderPageId = "x_priorityQuestions";
local.embeddedBuilderField = field.x_cr_priorityFormData;
Further down in the script, there are three lines marked with the comment UPDATE HERE. Change them to reference the Priority Score identifiers:
widget.x_priorityQuestions.setPageData(local.formBuilder); // UPDATE HERE!
widget.x_priorityFormContainer.setActiveLayoutName(local.setAssessmentLayout); // UPDATE HERE!
global.initialPriorityAssessmentData = CirrusLibPageData.getEmbeddedBuilderPageData(local.embeddedBuilderPageId, null); // UPDATE HERE!
The entire code will look like this:
#This interaction should be easy to copy from one object to another. Simply update the local variables at the top with the correct values for your object. #IMPORTANT! You may need to update certain logic steps that could not be generalized. Search for UPDATE HERE in the Script Editor tab for these steps. local.linkedFormField = "x_mrm_linkedPriorityForm"; local.embeddedBuilderPageId = "x_priorityQuestions"; local.embeddedBuilderField = field.x_cr_priorityFormData; #-------------------------------------------------------------------------------------- #Check if there is Form linked local.linkedForm = CirrusLibCirrusObjectLinkInstance.getLinkedObjectKeysFromField(local.linkedFormField, 1, null); local.isFormLinked = CirrusLibArray.hasAnyItems(local.linkedForm); local.isLoadActionSetDefault = CirrusLibString.strEquals(local.input_loadAction, "setDefault"); if (local.isFormLinked) { if (local.isLoadActionSetDefault) { local.formBuilder = global.defaultFormBuilder; } else { #Get the data from the embedded builder field in the linked Form local.getFormBuilderFromKey = CirrusLibCirrusObject.getObjectsByKey(local.linkedForm, "formBuilder", "Form", null); local.items = CirrusLibObject.get("items", local.getFormBuilderFromKey); local.formObj = CirrusLibArray.get(local.items, 0); local.formFields = CirrusLibObject.get("customFields", local.formObj); local.formBuilder = CirrusLibObject.get("formBuilder", local.formFields); } #Check that you successfully got the data from the embedded builder local.isAssessmentDataUINull = CirrusLibGeneral.isNullOrUndefined(local.formBuilder); local.isAssessmentDataUINotNull = CirrusLibBoolean.not(local.isAssessmentDataUINull); if (local.isAssessmentDataUINotNull) { #Load the embedded builder page screen widget.x_priorityQuestions.setPageData(local.formBuilder); #UPDATE HERE! If you copied this interaction from another object, you will need to update the ID of the widget that stores the EBP data. local.isAssessmentDataEmpty = CirrusLibString.isEmpty(local.embeddedBuilderField); local.isAssessmentDataNotEmpty = CirrusLibBoolean.not(local.isAssessmentDataEmpty); if (local.isAssessmentDataNotEmpty) { #If there is EBP data stored, load it into the embedded builder page screen CirrusLibPageData.setEmbeddedBuilderPageData(local.embeddedBuilderPageId, local.embeddedBuilderField); } } local.setAssessmentLayout = "assessment_layout"; } else { local.setAssessmentLayout = "zero_state_layout"; } widget.x_priorityFormContainer.setActiveLayoutName(local.setAssessmentLayout); #UPDATE HERE! If you copied this interaction from another object, you may need to update the ID of the widget that contains the EBP. #Update the global variable that stores the embedded builder page data global.initialPriorityAssessmentData = CirrusLibPageData.getEmbeddedBuilderPageData(local.embeddedBuilderPageId, null); #UPDATE HERE! If you copied this interaction from another object, you may need to update the ID of the widget that stores the EBP data.
Click Save.
This interaction checks whether a form is currently linked to the Change Request. If no form is linked, it switches the container to zero_state_layout and stops. If a form is linked, it fetches the form's formBuilder template, hands it to the EBP, restores any previously saved answers, switches the container to assessment_layout, and captures the current EBP state into a global variable for later reference.
Note: If you opened a brand new Change Request right now, you would still see nothing. That is expected. The routine exists but nothing is calling it yet.
x_priorityAssessmentFormAdded
When a user picks a form from the relationship component, the page needs to react: clear any stale state from a previously linked form, then load the new one. This is the on-add handler.
Find mrm_onApprovalChecklistAdded. Copy it. Rename the copy to x_priorityAssessmentFormAdded.
In the Script Editor, update these identifiers:
CirrusLibPage.showLoadingOverlay(null, null); #Clear out the form and the embedded builder page data just to be safe field.x_cr_priorityFormData = ""; local.emptyObject = CirrusLibGeneral.setNull(); widget.x_priorityQuestions.setPageData(local.emptyObject); CirrusLibPageData.clearEmbeddedBuilderPageData("x_priorityQuestions"); #Load the data from the embedded builder field in the linked form interactions.x_loadPriorityAssessmentData(null); CirrusLibPage.hideLoadingOverlay();
Click Save.
The defensive clear at the top matters. The EBP and the jsonObject field may still be holding state from a previously linked form. Without the clear, stale data could populate the EBP before the new form's template has a chance to render. The loading overlay around the work prevents the user from interacting with a half-drawn page.
x_priorityAssessmentFormRemoved
This interaction is the mirror image of the on-add handler. When the user removes a form, we have to undo everything that linking one did: clear the saved answers, clear the form from the screen, clear the priority score, and flip the page back to the empty view. If we skip this, the page ends up in a weird half-state.
When a user unlinks a form from the Change Request, everything the add path built up needs to be torn down. The jsonObject field, the EBP page data, and the Priority Score field all have to be cleared, and the container needs to flip back to zero_state_layout.
Find mrm_onApprovalChecklistRemoved. Copy it. Rename the copy to x_priorityAssessmentFormRemoved.
In the Script Editor, update these identifiers:
local.emptyObject = CirrusLibGeneral.setNull(); widget.x_priorityQuestions.setPageData(local.emptyObject); CirrusLibPageData.clearEmbeddedBuilderPageData("x_priorityQuestions"); field.x_cr_priorityFormData = ""; widget.x_priorityFormContainer.setActiveLayoutName("zero_state_layout");
Click Save.
Add and remove have to be symmetric. If the add path hydrates three stores: the answers, the EBP, and the score, the remove path has to clear all three. Otherwise unlinking leaves the page in an inconsistent state where the relationship is empty but the form's old questions, answers, or priority score are still visible.
Wiring the item actions on the relationship component
So far, we have written the routines but no one is calling them yet. The relationship picker is where the user actually attaches or removes a form, and it carries event slots for each of those moments. We now point each slot at the right routine.
The interactions exist as definitions on the page, but creating an interaction is not the same as telling Cirrus when to run it. The relationship component carries an item action slot for each lifecycle moment, and until those slots are populated, none of the interactions we just wrote will ever fire.
Click the x_mrm_linkedPriorityForm relationship component. In the Properties menu, find the Item actions section and assign:
| Item action | Assignment | What it does |
| getEditLinkForObjectInstance | mrm_getEditLinkForObjectInstance | Builds the URL for the linked form so clicking its name on the page navigates to the right form record. Reusing the shared MRM helper means if Form URLs ever change, every relationship in the solution picks up the fix automatically. |
| onAddedLinkInstances | x_priorityAssessmentFormAdded | Fires the moment a user picks a form. Clears stale state from any previously linked form, then calls the loader to fetch the new form's template and render it in the EBP. |
| onRemovedLinkInstances | x_priorityAssessmentFormRemoved | Fires the moment a user unlinks the form. Clears the saved answers, blanks the Priority Score field, and flips the container back to zero state. |
| getDraftLink | (leave empty) | Only meaningful when users draft a brand-new form inline from the picker. The Priority Score form is selected from a library of existing form templates, not drafted, so this slot has nothing to do. |
| onDeleteObjectInstances | (leave empty) | Fires when the linked form object itself is deleted, not merely unlinked. Form templates are owned by administrators — the Change Request never deletes them, so this slot stays empty. |
The two empty slots are intentional. The Priority Score form is linked from a library of existing form templates, not drafted inline, and the Change Request does not own the Form's lifecycle. Leaving these empty matches the shape of mrm_linkedAssessmentForm on the Model Candidate Assessment page.
Click Save.
Click the relationship component again and confirm all three assignments persisted. If any slot reverted to [none selected], reassign and save again before moving on. This single check catches the most common reason a fully-built assessment shows nothing on the page.
Calling the loader from the page entry points
The on-add handler only fires when something changes. Without a separate hook for that, the user would see the linked form's name in the picker but a blank area where the questions should be. We need to call the loader from every way the page can open.
At this point, adding a form to a Change Request will make the assessment layout appear immediately. The trouble is that this only works for the session in which the link was first made. The next time the Change Request opens, the relationship row still shows the linked form, but the container reverts to zero_state_layout and the EBP is blank. The add handler is the wrong hook for this case. Nothing was just added, the user is simply reopening a record that already has a form linked.
The fix is to call the loader on page load as well.
The Approval Checklist follows this same pattern: mrm_loadApprovalChecklistData is called from both mrm_onChangeRequestEditLoad and mrm_onChangeRequestDraftLoad. We need to add x_loadPriorityAssessmentData to the same entry points.
Page entry-point interactions are out-of-the-box. Editing them directly would couple our customizations to files the platform owns, and our changes would be silently overwritten the next time MRM ships an update. Instead, the platform supports an override pattern: if you create an interaction with the same name prefixed by x_, Cirrus runs the override in place of the OOTB version.
In the Interactions Editor, find mrm_onChangeRequestEditLoad. Copy it. Rename the copy to x_mrm_onChangeRequestEditLoad.
In the Script Editor, locate the line that reads interactions.mrm_loadApprovalChecklistData(...) and add the following immediately after it:
interactions.x_loadPriorityAssessmentData(null);
Click Save. Repeat the same copy-and-override for these load entry points:
| OOTB interaction | Override (create as a copy) | When it fires |
| mrm_onChangeRequestEditLoad | x_mrm_onChangeRequestEditLoad | Opening an existing Change Request in edit state |
| mrm_onChangeRequestDraftLoad | x_mrm_onChangeRequestDraftLoad | Opening a Change Request in draft state |
In each override, add the interactions.x_loadPriorityAssessmentData(null); line immediately after the existing approval checklist loader call.
Select any image to see a larger version.
Mobile users: To view the images, select the "Full" version at the bottom of the page.
Click Save on the interactions and then on the page. Add a relevant commit message.
Previewing the result
Navigate to Manage Versions, select your most recent commit, and click Preview.
Open a Change Request and switch to the Priority Score tab.
Before linking a form, the zero state layout shows and the relationship picker is visible. When you pick a form, the container should immediately flip to the assessment layout and the form renders inside the EBP. Answering questions updates the Priority Score dropdown in real time as triggerParent fires for each change.
Save the Change Request, navigate away, and reopen it: the EBP appears immediately with prior answers restored, the edit-load override doing its job. Unlink the form: the container returns to zero state, the EBP clears, and the Priority Score field resets to empty.
Use this table as a checklist:
| What you do | What should happen |
| Before picking a form | Picker visible. Where the questions would go is blank. |
| Pick a form from the dropdown | Questions appear immediately. Picker shows the chosen form's name. |
| Answer some questions | Priority Score updates as you go (Low, Medium, High, or Critical) |
| Save, close, reopen | Form is still there with answers filled in. Priority still showing. |
| Remove the form from the picker | Questions disappear. Priority field clears. Page returns to empty state. |
After saving the priority score, view the Priority Score column in the inventory table. This is the result of the dropdown field we created in Part 2. You'll be able to use this field in the dashboard report as well.
If something does not work
Most problems with a first-time build trace back to one specific thing: a slot on the relationship component that did not save its assignment. Open the page in Cirrus Builder, click x_mrm_linkedPriorityForm, and look at the five slots. Three of them should be filled in, two empty. If any of the three are showing [none selected], that is almost certainly the problem. Reassign and save again, and retest.
Other things to check, in order:
If none of these explain it, the fastest debug move is to open the Approval Checklist on the same page in Cirrus Builder and compare it side-by-side with what you built. The two should look identical except for names.
Across this three-part series, we built a working Priority Score assessment: a functional form in Part 1, the object-side plumbing and page structure in Part 2, and the interactions that bring everything to life in Part 3.
The Change Request page now opens with a Priority Score tab that reacts to every lifecycle moment — zero state on first open, rendered assessment when a form is linked, real-time score updates as the user answers, persisted answers across sessions, and clean teardown when the form is unlinked. None of it required custom services or external code. Everything is authored in Cirrus Builder, bound to the right component event slots, and wrapped in OOTB-safe overrides. The same pattern carries to any other assessment on any other object. Simply swap the form for a materiality questionnaire, a validation scorecard, or a governance review and the wiring is identical.
To download all asset files, click here.
How to Use:
To view more posts about SAS Model Risk Management, click here.
Find more articles from SAS Global Enablement and Learning here.
Visit the Tips & Tricks page for setup guidance, demos, and practical examples that show how Copilot supports your workflows.
Get Started →The rapid growth of AI technologies is driving an AI skills gap and demand for AI talent. Ready to grow your AI literacy? SAS offers free ways to get started for beginners, business leaders, and analytics professionals of all skill levels. Your future self will thank you.