Martin Virag
Tera Guru

Hi Everyone!

I've been working with Playbooks for some time now, and I'd like to highlight a common issue and its resolution when users start using Playbooks.

 

For this demonstration, I'm using a Yokohama PDI with a simple Playbook that runs whenever a CSM record is inserted into the sn_customerservice_case table. The Playbook contains only one activity: a User Form that displays the form. I also pass the Assignment Group and Assigned To values from the originating record.

 

The Assignment Group is Customer Service Support, which includes Abel Tuter as a member, and Customer Service Local, which includes Abraham Lincoln.

 

The case is currently assigned to Abel Tuter.

 

As you can see, Abel Tuter, as the owner of the activity, has access to all declarative actions — he can Mark as Complete, Skip, and Update — whereas Abraham Lincoln can only Update.

 

Abel's view:

Abel Tuter - OK.png

 

Abraham's view:

 

Abraham Lincoln - only Update.png

 

 

 

To understand the underlying reason it's important to introduce with 2 important components of a playbook.

 

The Context Record (or Associated Record) - This is the "current" record, or the record whose fields we want to display within a Playbook activity. An example would be a Case record.

 

The Flow Data Record (sys_flow_data) - This is the default experience status mapping record. In short, the state of this record determines the state shown on a Playbook card, what actions are allowed, and more. You can find additional information in this docs link.

 

The reason Abraham does not have access to all actions and cannot close the activity is because, when the action was created in the sys_flow_data record, Abel was set as the Assigned To. The sys_flow_data table has its own ACLs that evaluate access based on the Assigned To and Assignment Group fields.

 

sys_flow_data_activity.png

 

 

 

 

sys_flow_acl.png

 

 

 

 

 

And this is where our Problem statement lies.

 

When I reassign the Assignment Group and Assigned To on the Case record (i.e., the Context or Associated Record) for any process reason, the sys_flow_data record’s Assignment Group and Assigned To fields do not update automatically. This leads to a situation where the person intended to work on the given Playbook action is unable to access crucial declarative actions required for their job — such as completing the activity.

You might say, “Well, that’s easy — just create a Business Rule that updates the Assignment Group and Assigned To when it changes on the original record.”

And you'd be completely right. However, tracking the connection between the Context Record and the Flow Data record is a multi-step process.

 

To achieve this - seemingly - simple task , I’d like to walk you through several approaches I've taken, along with their pros and cons.

 

 

Solution 1)

We assume here that you want to keep everything as close to OOB as possible.

 

The advantages of this approach are:

  • You can use the OOB actions provided by ServiceNow as-is.

  • No additional table fields need to be maintained.

The disadvantages of this approach are:

  • Finding the experience status record is cumbersome and requires multiple lookup operations.

  • It only works well if an associated record is added.

To find the experience record, the following high-level steps need to be executed:

 

Step 1: Go to the sys_pd_context table and locate the Parent Record using the input_record field — this will be the Case that triggered the entire Playbook.

 

step1.png

 

Step 2: Go to the sys_pd_activity_context table and filter by the context field where the sys_id matches the record from Step 1, and where the associated_record field matches the sys_id of the current associated record (which could again be a Case, Case Task, or any record type the Playbook is running on).

 

step2.png

 

Step 3:From the record retrieved in Step 2, you can dot-walk to the field type_vals.experience_status_record, which will return the sys_id of the corresponding sys_flow_data experience status record.

 

Side note: On the record itself, you can use the "Show Activity Context Vals" related link. However, in scripts, you can access the parameter via a simple dot-walk, as this is a Glide Variable (glide_var) type field.

 

step3.png

Step 4: Finally, go to the sys_flow_data table and look for the record where the sys_id matches the value retrieved in Step 3.

 

step4.png

 

 

Okay, now let’s build this logic into a Script Include. You can enhance this basic script with error handling as needed.

I personally love JSDoc-style documentation — and yes, I used an LLM to generate these docs. 😄

 

var playBookUtils = Class.create();
playBookUtils.prototype = {
    initialize: function() {
    },

	 /**
 * Updates the Assignment Group and Assigned To values of an experience status record
 * based on the provided context and current record sys_ids.
 *
 *  {string} contextSysId - Sys_id of the input context (e.g., Case).
 *  {string} currentSysId - Sys_id of the current record (e.g. case, case task).
 *  {string} agSysId - Sys_id of the Assignment Group to be set.
 *  {string} atSysId - Sys_id of the user to be assigned.
 */
_updateFdRecord: function(contextSysId, currentSysId, agSysId, atSysId) {
    var pdContextSysId = this._getContext(contextSysId);
    var expStatusRecordSysId = this._getActivityContext(pdContextSysId, currentSysId);
    this._changeAG(expStatusRecordSysId, agSysId);
    this._changeAt(expStatusRecordSysId, atSysId);
},
/**
 * Retrieves the sys_id of an in-progress Playbook (Process Definition) context record
 * that references the specified input record.
 *
 *  {string} contextSysId - Sys_id of the input record (e.g., Case, Quote, Order).
 * @returns {string|undefined} The sys_id of the matched sys_pd_context record, or undefined if not found.
 */
_getContext: function(contextSysId) {
    var contextGr = new GlideRecord("sys_pd_context");
    contextGr.addQuery("input_record", contextSysId);
    contextGr.addQuery("state", "IN_PROGRESS");
    contextGr.query();
    while (contextGr.next()) {
        return contextGr.sys_id;
    }
},
/**
 * Retrieves the sys_id of the experience status record associated with a specific
 * activity context, based on the Playbook context and the current record.
 *
 *  {string} pdContextSysId - Sys_id of the sys_pd_context record.
 *  {string} currentSysId - Sys_id of the current associated record (e.g., playbook task).
 * @returns {string|undefined} The sys_id of the experience status record, or undefined if not found.
 */
_getActivityContext: function(pdContextSysId, currentSysId) {
    var activityGr = new GlideRecord("sys_pd_activity_context");
    activityGr.addQuery("context", pdContextSysId);
    activityGr.addQuery("associated_record", currentSysId);
    activityGr.query();
    while (activityGr.next()) {
        return activityGr.type_vals.experience_status_record;
    }
},
/**
 * Updates the Assignment Group of the specified experience status record.
 *
 *  {string} expStatusRecordSysId - Sys_id of the experience status record (sys_flow_data).
 *  {string} agSysId - Sys_id of the new Assignment Group to set.
 */
_changeAG: function(expStatusRecordSysId, agSysId) {
    var flowDataGr = new GlideRecord("sys_flow_data");
    flowDataGr.get(expStatusRecordSysId);
    flowDataGr.setValue("assignment_group", agSysId);
    flowDataGr.update();
},
/**
 * Updates the Assigned To user of the specified experience status record.
 *
 *  {string} expStatusRecordSysId - Sys_id of the experience status record (sys_flow_data).
 *  {string} atSysId - Sys_id of the new user to assign the record to.
 */
_changeAt: function(expStatusRecordSysId, atSysId) {
    var flowDataGr = new GlideRecord("sys_flow_data");
    flowDataGr.get(expStatusRecordSysId);
    flowDataGr.setValue("assigned_to", atSysId);
    flowDataGr.update();
},


    type: 'playBookUtils'
};

 

And here is the Business Rule, which is triggered after update on the sn_customerservice_case table when the Assignment Group or Assigned To fields change.

 

I pass current.sys_id as both the context and current parameters in the _updateFdRecord method, because all my operations are running at the Case level.

 

(function executeRule(current, previous /*null when async*/) {
    try{
        new sn_customerservice.playBookUtils()._updateFdRecord(current.sys_id, current.sys_id, current.assignment_group, current.assigned_to);
    }catch(e){
        gs.info("An error occured when tried to add the assignment group or assigned to to the playbook");
    }
})(current, previous);

 

You can use a similar approach in Subflows if you prefer a low-code solution.

 

If you need to handle an activity that doesn’t have an associated record, you can use the label field in the sys_pd_activity_context table. However, be aware that this value can change — I haven’t found a better alternative yet. If you have, please share it in the comments!

 

This -in my opinion - is the go-to solution if you want to maintain the OOB Activity Definitions and generally avoid introducing custom logic into your operations.

 

Solution 2)

 

In this approach, we assume that you’re allowed to modify the Out-of-the-Box (OOB) behavior but are reluctant to add extra fields to the sys_flow_data table.

 

The advantages of this approach are:

 

  • No additional table fields need to be maintained.

  • The associated record’s sys_id can be directly accessed from the sys_flow_data record (when browsing the table).

  • It allows you to support multiple different use cases without adding extra fields to the sys_flow_data table.

The disadvantage of this approach is:

 

  • Every OOB Activity Definition needs to be extended with additional logic.

For our second solution, we use a feature called Data Definition Variables. You can read more about them here.

 

We’re essentially creating a new Data Definition and preparing it to accept a Document ID and a Table Name, because our goal is always to design solutions that are scalable and reusable across multiple use cases. (For our demo, the Sys ID alone would be sufficient.)

 

data_definition.png

 

The next step is to update the Activity Definition’s automation subflow. For the OOB User Form, this subflow is called "Manual Activity."

 

However, before we do that, we need to create a custom action, because currently there is no OOB way to populate a Flow Data Variable from Flow Designer.

 

populate flow data variables.png

 

My custom action can accept a variable name and either a string value or an HTML value (because again, scalability and reusability) . The HTML value is particularly useful when working with variables you plan to include in an email body later on.

 

Code snippet:

(function execute(inputs, outputs) {
// ... code ...
var vName = inputs.vName;
var vValue = inputs.vValue;
var fSysID = inputs.fDataSysID;
var htmlValue = inputs.htmlValue;

var fDataGR = new GlideRecord("sys_flow_data");
fDataGR.get(fSysID);

if (vValue){
fDataGR.vars[vName] = vValue;
}
if (htmlValue){
    fDataGR.vars[vName] = htmlValue;
}

fDataGR.update();



})(inputs, outputs);

 

Now, modify the OOB Manual Activity.

First, I added two additional inputs: Document ID and Table Name. These parameters are expected to be passed in by the Process at runtime.

 

inputs.png

 

Next, I selected the new Flow Data Definition and populated the two variables.

 

Flow Data Add.pngexample variable.png

 

Remember, the variable names here must match the ones you defined when you created the new Data Definition record earlier.

 

The next step is to copy the Activity Definition. Unfortunately, all OOB Activity Definitions have a read-only policy, so manually recreating them is unavoidable. For this I made a copy of the User Form Activity Definition and selected the newly created Subflow in the automation plan.

 

automation_input.png

 

I preconfigured the table_name and document_id for the configuration record, but these can be changed later in Playbook Designer if needed.

 

Don’t forget to add some declarative actions as well — otherwise, the Playbook action will be read-only!

 

Here's the designer view:

automation_playbook_designer.png

 

 

After executing the Subflow, we can now see that the Flow Data record contains the two variables:

 

data_definition_variables.png

 

This functionality is also very useful for other use cases, as it allows you to pass additional data and values required by your process logic. Moreover, you can use these variables as data providers for your custom UIs.

These variables are stored in the sys_variable_value table, which allows us to directly link the flow_data record with the context record.

 

However, we need an additional get operation to ensure we only update records that are active.

 

Here is the After Business Rule script I used:

(function executeRule(current, previous /*null when async*/) {

var gr = new GlideRecord("sys_variable_value");
gr.addQuery("value", current.sys_id);
gr.addQuery("document", "sys_flow_data");
gr.query();
while (gr.next()) {
    var fData = new GlideRecord("sys_flow_data");
    
	if (fData.get(gr.document_key)) {
        if (fData.state == "PENDING" || fData.state == "IN_PROGRESS") {
            fData.setValue("assignment_group", current.getValue("assignment_group"));
			fData.setValue("assigned_to", current.getValue("assigned_to"));
			fData.update();

        }
    }
}

})(current, previous);

 

 

Solution 3) 

 

Finally, the last solution is to add two fields directly to the sys_flow_data table to identify the context record.

 

The advantages of this approach are:

 

  • The fields directly store the context record identifier on the sys_flow_data table, making it easy to identify.

  • It’s a very straightforward and simple approach.

 

The disadvantages of this approach are:

 

  • Every OOB Activity Definition must be extended with additional logic.

  • It does not support dynamically responding to different use cases without adding more fields to the sys_flow_data table.

In this solution, I added two extra fields to the sys_flow_data table: one for the Table Name and one for the Document ID.

I also created a reusable Subflow that can be connected to any flows needing to update the Flow Data context record — because reusability is king.

 

Picture of Subflow:

 

Subflow Update FD Record.png

 

As the next step, let’s make a copy of the previously created Subflow and Activity Definition, and connect the new Subflow to it.

 

Demo Flow activity w Table.png

 

 

Once the new Activity Definition is configured and the Playbook is executed, the fields are successfully populated on the sys_flow_data record.

 

table_record_new.png

Finally, the following Business Rule can be used to easily update the sys_flow_data record.

 

(function executeRule(current, previous /*null when async*/) {

var gr = new GlideRecord("sys_flow_data");
gr.addEncodedQuery("u_document=" + current.sys_id +"^stateINPENDING,IN_PROGRESS");
gr.query();
while (gr.next()) {
	gr.setValue("assigned_to", current.assigned_to);
	gr.setValue("assignment_group", current.assignment_group);
	gr.update();
}

})(current, previous);

 

 

Automating updates to the Assignment Group and Assigned To fields in Playbooks can be challenging, especially when trying to stay close to the OOB functionality. In this article, I introduced three different approaches to solving this problem, each with its own benefits and limitations.

 

Whether you choose to work with existing structures, use Data Definition variables, or add custom fields to the sys_flow_data table, the best solution depends on your use case, your flexibility in modifying OOB components, and how much reusability you want in your setup.

 

Understanding how Flow Data works behind the scenes is key to building scalable and maintainable automation with the Playbooks.

 

Stay tuned for the next article, where I’ll show you how to build a custom UI to manage multiple referenced Tasks more efficiently.

Comments
Daniel_san
Tera Expert

Brilliant! Thanks for the contribution!


I'm sharing my use case in case it helps someone else (based on a variant of the first solution you provided):

  • My need was to allow the "Questionnaire" activity in a playbook to be answered by members of the sc_task.assignment_group from which the playbook originates — even when the assignment group changes (the same underlying issue you described, where only the initially assigned group's members could respond when the playbook was created).

  • In this case, the field sys_pd_activity_context.associated_record directly contains the sys_flow_data reference that needs to be updated (instead of pointing to a parent record like in your described case, for a "User form" activity).

So this final "Script Include" allowed me to finally see the light!

var playbookTaskUtils = Class.create();
playbookTaskUtils.prototype = {
    initialize: function () {},
    updateFlowDataAssignmentGroup: function (taskSysId, newAssignmentGroupSysId) {

        // Step 1: Retrieve the playbook context where input_record matches the task
        var contextGR = new GlideRecord("sys_pd_context");
        contextGR.addQuery("input_record", taskSysId);
        contextGR.query();

        if (!contextGR.next())
            return;

        var contextId = contextGR.getUniqueValue();

        // Step 2: Find all activity contexts within this playbook context that match the desired activity definition
        var activityContextGR = new GlideRecord("sys_pd_activity_context");
        activityContextGR.addQuery("context", contextId);
        activityContextGR.addQuery("activity_definition", "cdb5cd6f777b71106ed97a8c8c5a9957"); // Target activity definition
        activityContextGR.query();

        // Step 3: For each match, update the assignment_group in the linked sys_flow_data record
        while (activityContextGR.next()) {
            var flowDataId = activityContextGR.getValue("associated_record");

            var flowDataGR = new GlideRecord("sys_flow_data");
            if (flowDataGR.get(flowDataId)) {
                flowDataGR.setValue("assignment_group", newAssignmentGroupSysId);
                flowDataGR.update();
            }
        }
    },

    type: "playbookTaskUtils"
};


 

Martin Virag
Tera Guru

Hi @Daniel_san !

Thank you for the addition! Yes, it can happen definately and here is why:  The Questionnaire type Activity definition introduced in Yokohama and previously we have had to deal with variables and rendered these variables by selecting the sys_flow_data table as the associated record and selected the view data_collection. You can see examples of this if you install any case playbooks from the store (check for e-mail activity with user input 🙂 ). 

My logical conclusion is that the Questionnaire type activity actually unified this by setting the associated record automatically 🙂 

...aand by checking the activity definition it is indeed true

Screenshot 2025-07-10 at 16.42.02.png



Travis Rogers
ServiceNow Employee
ServiceNow Employee

Martin - Nice article. I'm looking forward to your next one on custom UIs for multiple referenced records. 

Version history
Last update:
‎05-25-2025 11:40 AM
Updated by:
Contributors