Основной код
/**
*
* @param {GoogleAppsScript.Spreadsheet.Range} range
*/
function updateHistNotesByRange_(range) {
const values = range.getDisplayValues();
const notes = range.getNotes().map((row) =>
row.map((cell) => {
const [last, ...rest] = cell.split('✅');
return { last, rest };
}),
);
const newNotes = values.map((row, i) =>
row.map((cell, j) => {
const res = [notes[i][j].last, ...notes[i][j].rest];
if (cell !== notes[i][j].last) res.unshift(cell);
return res.join('✅');
}),
);
range.setNotes(newNotes);
}
Идея в том, что вы передаете в функцию диапазон и он обновляется как надо.
Пример использования при изменении данных пользователем
/**
*
* @param {{range: GoogleAppsScript.Spreadsheet.Range}} e
*/
function onEditTrigger(e) {
const range = e.range;
const sheet = range.getSheet();
if (sheet.getName() !== 'Запись предыдущего значения в заметки при изменении') return;
updateHistNotesByRange_(e.range);
}
onEditTrigger
нужно зарегистрировать как триггер на изменение
Или пример сохранения с периодичностью
function timeBasedTrigger() {
const range = SpreadsheetApp.getActive().getSheetByName('Запись предыдущего значения в заметки').getDataRange();
updateHistNotesByRange_(range);
}
timeBasedTrigger
нужно зарегистрировать как триггер времени
Эти подходы можно комбинировать в части размеров диапазонов, например, при изменении ячейки, входящей в конкретный диапазон нужно обновлять весь диапазон. Обычно это касается формул.
Пример работы

Код в Таблице https://docs.google.com/spreadsheets/d/1qDD9P6zMXImOqlEaEtIa2XzKZjGVOXcJ7S4nmPpIWvY/edit#gid=106780295&range=D2