Use PDIs? Take our 5-minute survey to help shape the PDI roadmap.

Best practices for updating large volumes of data using Script Action

MizukiW
Tera Contributor

コントロール(最大7万件)のStateをAttestに一括で変更したいのですが、120件以上選択して[Attest]ボタンを押すと、処理が途中で終了します。
そのため、非同期処理に変更したいと思っています。

 

実装イメージ:List Action[Attest]でEventを生成し、Script ActionでStateをAttestに更新する処理を実施。

 

質問:

①Script Actionで大量データを更新するときのベストプラクティスを教えてください。

②一括State変更の際、UpdateMultipleを使用しようと思ったのですが、AIに聞くとビジネスルールが動かない(=Attestationが生成できない)と言っています。これは本当でしょうか?

 

***********************************************************************************************

I want to bulk-update the "State" of records (up to 70,000 items) to "Attest," but the process terminates prematurely if I select more than 120 items and click the [Attest] button.
Therefore, I would like to switch to an asynchronous process.

The intended approach is to generate an event using the [Attest] button (List Action) and then update the state to "Attest" via a Script Action.

① Please tell me the best practices for updating large volumes of data using a Script Action.

② I considered using `UpdateMultiple` for the bulk state change, but AI informed me that business rules do not execute (meaning attestations cannot be generated) when using it. Is this true?

1 REPLY 1

Abhishek Pal
Giga Guru

Hi @MizukiW ,

Your event + Script Action approach is valid, but I would make one important change:

Do not process all 70,000 records in a single Script Action execution.

Moving the operation to an event makes it asynchronous from the user's browser, but the Script Action itself still has to execute server-side. Processing 70,000 individual updates in one execution can still become a very long transaction and can put significant load on the instance.

Recommended architecture:

List Action
-> Create Bulk Attest Batch
-> Queue Event
-> Script Action
-> Process 200-500 records
-> Queue next event
-> Continue until complete

1. Do not send 70,000 sys_ids through event.parm1

If the user is effectively selecting all records matching a list filter, store the encoded query in a small custom batch record.

For example:

u_bulk_attest_batch

Fields:

u_table
u_encoded_query
u_target_state
u_processed
u_status
u_requested_by
u_started
u_completed

Then queue only the batch sys_id:

gs.eventQueue(
'x_your_scope.bulk_attest.process',
batchGR,
batchGR.getUniqueValue(),
''
);

Script Actions are specifically designed to execute server-side logic in response to queued events.

If users can select an arbitrary set of records that cannot be represented by a filter, do not place tens of thousands of sys_ids into parm1/parm2. Store the selection in a staging/batch-item table instead.

2. Process the records in chunks

For example, use a chunk size of 250 initially.

Script Action example:

var batchId = event.parm1 + '';

var batchGR = new GlideRecord('u_bulk_attest_batch');

if (!batchGR.get(batchId))
return;

if (batchGR.getValue('u_status') == 'complete')
return;

var tableName =
batchGR.getValue('u_table');

var encodedQuery =
batchGR.getValue('u_encoded_query');

var targetState =
batchGR.getValue('u_target_state');

var chunkSize = 250;
var processed = 0;

var gr = new GlideRecord(tableName);

gr.addEncodedQuery(encodedQuery);

// Prevent already processed records
// from being selected again.
gr.addQuery('state', '!=', targetState);

gr.setLimit(chunkSize);
gr.query();

while (gr.next()) {

gr.setValue(
'state',
targetState
);

try {

var result =
gr.update();

if (result)
processed++;

} catch (ex) {

gs.error(
'Bulk Attest failed for ' +
gr.getUniqueValue() +
': ' +
ex.message
);
}
}

var totalProcessed =
parseInt(
batchGR.getValue('u_processed') || '0',
10
);

batchGR.setValue(
'u_processed',
totalProcessed + processed
);

if (processed == chunkSize) {

batchGR.setValue(
'u_status',
'processing'
);

batchGR.update();

// Queue only the next chunk.
gs.eventQueue(
'x_your_scope.bulk_attest.process',
batchGR,
batchGR.getUniqueValue(),
''
);

} else {

batchGR.setValue(
'u_status',
'complete'
);

batchGR.setValue(
'u_completed',
new GlideDateTime()
);

batchGR.update();
}

The important part is:

gr.update();

rather than one updateMultiple() call.

This allows each record to go through its normal update processing so your existing attestation logic can execute.

3. Regarding updateMultiple()

The statement:

"updateMultiple() never runs Business Rules"

is too absolute.

ServiceNow has documented updateMultiple() as a bulk update method, and a ServiceNow employee has explained that internally it can execute iteratively when certain table characteristics exist, including before/after Business Rules.

Under other conditions, the platform can use a bulk GlideMultipleUpdate operation.

Because this internal behavior depends on table/configuration characteristics, I would NOT design an attestation process that relies on updateMultiple() triggering one Business Rule for every record.

Your requirement specifically depends on:

State -> Attest
-> Business Rule
-> Generate Attestation

Therefore use:

gr.update()

for each record.

That gives you predictable per-record processing.

4. Do not use setWorkflow(false)

Do not do this:

gr.setWorkflow(false);

because your requirement depends on the Business Rules / processing that generate the attestations.

Using setWorkflow(false) would specifically work against that requirement.

5. Why I would not update 70,000 records simultaneously

Consider what happens when each record reaches Attest.

Potentially each update can generate:

Business Rules
Events
Audit entries
Attestation records
Async Business Rules
Notifications
Other related processing

70,000 records can therefore generate substantially more than 70,000 database operations.

Chunking gives the scheduler/event processor room to process the downstream work instead of creating one very large transaction.

Start with approximately:

200-500 records per chunk

and performance-test the actual value in sub-production.

The correct number depends on what the Attest transition executes in your instance.

6. Avoid offset-based pagination

I would not use:

chooseWindow()

to process:

0-500
501-1000
1001-1500

etc.

Since each successfully processed record changes to Attest, make your batch query exclude:

State = Attest

Then each new event naturally retrieves the next unprocessed group.

This avoids expensive paging/count operations on a large table.

7. Add operational controls

For a production implementation I would also store:

Status:
Pending / Processing / Complete / Failed

Processed count

Failed count

Requested by

Started

Completed

This gives administrators visibility into long-running operations and lets you prevent the same bulk operation from being launched repeatedly.

You can also create a related batch-error table if individual records fail.

Recommended final design:

User clicks Attest
-> Validate role/access
-> Store batch/filter
-> Queue one event
-> Immediately return control to user
-> Script Action processes 250 records
-> Standard gr.update()
-> Existing Attestation Business Rules execute
-> Queue next chunk
-> Repeat
-> Mark batch Complete

So to answer your two questions directly:

1. Yes, Event + Script Action is appropriate, but process the records in small chained batches rather than all 70,000 in one Script Action.

2. Do not assume updateMultiple() will always bypass or always execute Business Rules. Its internal behavior can differ. Since attestation creation depends on per-record Business Rule processing, use individual update() calls.

Official ServiceNow references:

Script Actions:
https://www.servicenow.com/docs/r/platform-administration/system-events/r_ScriptActions.html

System Events:
https://www.servicenow.com/docs/r/platform-administration/system-events/events.html

GlideRecord API:
https://www.servicenow.com/docs/r/api-reference/server-api-reference/c_GlideRecordAPI.html

ServiceNow Community discussion containing ServiceNow employee explanation of updateMultiple():
https://www.servicenow.com/community/servicenow-ai-platform-forum/updatemultiple-doesn-t-update-syst...

Hope this helps!

If this response helped, please mark it as Helpful.
If it resolves your issue, please Accept it as Solution.

Kind Regards,
Abhishek Pal