Scripting and Macros


Office Scripts

link - learn.microsoft.com/en-us/office/dev/scripts/resources/samples/samples-overview 
link - learn.microsoft.com/en-us/office/dev/scripts/resources/samples/write-large-dataset
link - learn.microsoft.com/en-us/office/dev/scripts/resources/samples/external-fetch-calls
link - learn.microsoft.com/en-us/office/dev/scripts/resources/samples/filter-table-get-visible-range
link - learn.microsoft.com/en-us/office/dev/scripts/resources/samples/get-table-data

var _range = context.workbook.names.getItem("myNamedRange").getRange(); 
var _rangeValues = _range.getRow(1).getBoundingRect(_range.getLastCell());
var _myCol = _range.getColumn(0);

Get the value in cell A1.

let range = selectedSheet.getRange("A1"); 
console.log(range.getValue());

Get the current active cell

let cell = workbook.getActiveCell(); 
console.log(`The current cell's value is ${cell.getValue()}`);

getCell(0,0) = A1 
getRange
getWorksheet
getAddress
getValue - only 1 value, doesn't return an array from a larger range
getValues - returns an array
ExcelScript.Range ?
getRange.getSurroundingRegion

let myArray = wshName.getValues()
myArray.shift() // removes the header / first row

This script applies some formatting to the cell to its right and the cell above.

function main(workbook: ExcelScript.Workbook) { 
  let activeCell = workbook.getActiveCell();
  console.log(`The active cell's address is: ${activeCell.getAddress()}`);

  let rightCell = activeCell.getOffsetRange(0,1);
  rightCell.setValue("Right cell");
  console.log(`The right cell's address is: ${rightCell.getAddress()}`);
  rightCell.getFormat().getFont().setColor("Magenta");
  rightCell.getFormat().getFill().setColor("Cyan");

  let aboveCell = activeCell.getOffsetRange(-1, 0);
  aboveCell.setValue("Above cell");
  console.log(`The above cell's address is: ${aboveCell.getAddress()}`);
  aboveCell.getFormat().getFont().setColor("White");
  aboveCell.getFormat().getFill().setColor("Black");
}

This script copies the formatting from the active cell to the 9 neighbouring cells.
Note that this script only works when the active cell isn't on an edge of the worksheet.

function main(workbook: ExcelScript.Workbook) { 
  let activeCell = workbook.getActiveCell();
  let cornerCell = activeCell.getOffsetRange(-1,-1);
  let surroundingRange = cornerCell.getResizedRange(2, 2)
  surroundingRange.copyFrom(activeCell, ExcelScript.RangeCopyType.formats);
}

This script clears the formatting from the selected range, and loops through every cell changing the fill color to a random color.

function main(workbook: ExcelScript.Workbook) { 
  let range = workbook.getSelectedRange();
  let rows = range.getRowCount();
  let cols = range.getColumnCount();
  range.clear(ExcelScript.ClearApplyTo.formats);

  for (let row = 0; row < rows; row++) {
    for (let col = 0; col < cols; col++) {
      let colorString = `#${Math.random().toString(16).substr(-6)}`;
      range.getCell(row, col).getFormat().getFill().setColor(colorString);
    }
  }
}

This script gets the current used range and shades any blank cells with a yellow background.

function main(workbook: ExcelScript.Workbook) { 
    let range = workbook.getActiveWorksheet().getUsedRange();
    let blankCells = range.getSpecialCells(ExcelScript.SpecialCellType.blanks);
    blankCells.getFormat().getFill().setColor("yellow");
}

This script removes hyperlinks

function main(workbook: ExcelScript.Workbook, sheetName: string = 'Sheet1') {  
  // Get the active worksheet.
  let sheet = workbook.getWorksheet(sheetName);

  // Get the used range to operate on.
  // For large ranges (over 10000 entries), consider splitting the operation into batches for performance.
  const targetRange = sheet.getUsedRange(true);
  console.log(`Target Range to clear hyperlinks from: ${targetRange.getAddress()}`);

  const rowCount = targetRange.getRowCount();
  const colCount = targetRange.getColumnCount();
  console.log(`Searching for hyperlinks in ${targetRange.getAddress()} which contains ${(rowCount * colCount)} cells`);

  // Go through each individual cell looking for a hyperlink.
  // This allows us to limit the formatting changes to only the cells with hyperlink formatting.
  let clearedCount = 0;
  for (let i = 0; i < rowCount; i++) {
    for (let j = 0; j < colCount; j++) {
      const cell = targetRange.getCell(i, j);
      const hyperlink = cell.getHyperlink();
      if (hyperlink) {
        cell.clear(ExcelScript.ClearApplyTo.hyperlinks);
        cell.getFormat().getFont().setUnderline(ExcelScript.RangeUnderlineStyle.none);
        cell.getFormat().getFont().setColor('Black');
        clearedCount++;
      }
    }
  }
  console.log(`Done. Cleared hyperlinks from ${clearedCount} cells`);
}

This script remove duplicates from a sheet based on one or more key columns.

function main( 
  workbook: ExcelScript.Workbook,
  sheetName?: string,
  headerRowNumber?: number,
  keyColumnHeadersCsv: string = "Id"
) {
  const worksheet = sheetName
    ? workbook.getWorksheet(sheetName)
    : workbook.getActiveWorksheet();

  const usedRange = worksheet.getUsedRange();
  if (!usedRange) return;

  const allValues = usedRange.getValues() as (string | number | boolean)[][];
  const totalRowCount = allValues.length;
  const totalColCount = allValues[0].length;

  const headerRowIndex = Math.max(0, headerRowNumber - 1);

  if (headerRowIndex >= totalRowCount) {
    throw new Error("headerRowNumber is outside the used range.");
  }

  const headerValues = allValues[headerRowIndex].map(v => String(v ?? "").trim());

  const keyHeaders = keyColumnHeadersCsv
    .split(",")
    .map(h => h.trim());

  if (keyHeaders.length === 0) {
    throw new Error("Please provide at least one header name in keyColumnHeadersCsv.");
  }

  const keyColumnIndexes = keyHeaders.map(headerName => {
    const colIndex = headerValues.indexOf(headerName);
    if (colIndex < 0) {
      throw new Error(`Key column not found in header row: "${headerName}"`);
    }
    return colIndex;
  });

  const seenKeys = new Set<string>();

  const outputValues: (string | number | boolean)[][] = [];
  outputValues.push(allValues[headerRowIndex]);

  const firstDataRowIndex = headerRowIndex + 1;

  for (let rowIndex = firstDataRowIndex; rowIndex < totalRowCount; rowIndex++) {
    const rowValues = allValues[rowIndex];

    const uniqueKey = keyColumnIndexes
      .map(colIndex => String(rowValues[colIndex] ?? "").trim())
      .join("||");

    if (isEmptyKey(uniqueKey, keyColumnIndexes.length)) {
      outputValues.push(rowValues);
      continue;
    }

    if (!seenKeys.has(uniqueKey)) {
      seenKeys.add(uniqueKey);
      outputValues.push(rowValues);
    }
  }

  usedRange.clear(ExcelScript.ClearApplyTo.contents);

  const startRow = usedRange.getRowIndex();
  const startCol = usedRange.getColumnIndex();

  const outputRange = worksheet.getRangeByIndexes(
    startRow + headerRowIndex,
    startCol,
    outputValues.length,
    totalColCount
  );

  outputRange.setValues(outputValues);
}

function isEmptyKey(uniqueKey: string, keyColumnCount: number): boolean {
  if (keyColumnCount <= 1) return uniqueKey.trim() === "";
  return uniqueKey === "||".repeat(keyColumnCount - 1);
}

This script also remove duplicates with more advanced options

function main( 
  workbook: ExcelScript.Workbook,
  sheetName: string = "",
  headerRowNumber: number = 1,

  // Key columns define what counts as "duplicate"
  keyColumnHeadersCsv: string = "Id", // e.g. "EmployeeId,Region"

  // Choose how to decide which duplicate row to keep
  keepStrategy: "byRowOrder" | "byDate" = "byRowOrder",

  // Only applies to keepStrategy="byRowOrder"
  scanDirection: "topToBottom" | "bottomToTop" = "topToBottom",

  // Only applies to keepStrategy="byDate"
  dateColumnHeader: string = "" // e.g. "ModifiedOn" or "UpdatedAt"
) {
  const worksheet = sheetName
    ? workbook.getWorksheet(sheetName)
    : workbook.getActiveWorksheet();

  const usedRange = worksheet.getUsedRange();
  if (!usedRange) return;

  const allValues = usedRange.getValues();
  const totalRowCount = allValues.length;
  const totalColCount = allValues[0].length;

  const headerRowIndex = Math.max(0, headerRowNumber - 1);

  const headerValues = allValues[headerRowIndex].map(v => String(v ?? "").trim());

  const keyHeaders = keyColumnHeadersCsv
    .split(",")
    .map(h => h.trim());

  if (keyHeaders.length === 0) {
    throw new Error("Please provide at least one key column header (keyColumnHeadersCsv).");
  }

  const keyColumnIndexes: number[] = keyHeaders.map(headerName => {
    const colIndex = headerValues.indexOf(headerName);
    if (colIndex < 0) throw new Error(`Key column not found in header row: "${headerName}"`);
    return colIndex;
  });

  let dateColumnIndex = -1;
  if (keepStrategy === "byDate") {
    if (!dateColumnHeader) {
      throw new Error(`keepStrategy="byDate" requires a dateColumnHeader.`);
    }
    dateColumnIndex = headerValues.indexOf(dateColumnHeader);
    if (dateColumnIndex < 0) {
      throw new Error(`Date column not found in header row: "${dateColumnHeader}"`);
    }
  }

  const keptRowIndexByKey = new Map<string, number>();

  const firstDataRowIndex = headerRowIndex + 1;
  const lastDataRowIndex = totalRowCount - 1;

  const rowScanOrder: number[] = [];
  if (scanDirection === "topToBottom") {
    for (let rowIndex = firstDataRowIndex; rowIndex <= lastDataRowIndex; rowIndex++) {
      rowScanOrder.push(rowIndex);
    }
  } else {
    for (let rowIndex = lastDataRowIndex; rowIndex >= firstDataRowIndex; rowIndex--) {
      rowScanOrder.push(rowIndex);
    }
  }

  for (const rowIndex of rowScanOrder) {
    const rowValues = allValues[rowIndex];

    const uniqueKey = keyColumnIndexes
      .map(colIndex => String(rowValues[colIndex] ?? "").trim())
      .join("||");

    if (!uniqueKey || uniqueKey === "||".repeat(Math.max(0, keyColumnIndexes.length - 1))) {
      continue;
    }

    if (!keptRowIndexByKey.has(uniqueKey)) {
      keptRowIndexByKey.set(uniqueKey, rowIndex);
      continue;
    }

    if (keepStrategy === "byRowOrder") {
      continue;
    }

    const currentlyKeptRowIndex = keptRowIndexByKey.get(uniqueKey)!;

    const keptTime = toComparableTime(allValues[currentlyKeptRowIndex][dateColumnIndex] as any);
    const candidateTime = toComparableTime(rowValues[dateColumnIndex] as any);

    if (candidateTime > keptTime) {
      keptRowIndexByKey.set(uniqueKey, rowIndex);
    }
  }

  const keptRowIndicesSorted = Array.from(keptRowIndexByKey.values()).sort((a, b) => a - b);

  const outputValues: (string | number | boolean)[][] = [];
  outputValues.push(allValues[headerRowIndex] as (string | number | boolean)[]);
  for (const rowIndex of keptRowIndicesSorted) {
    outputValues.push(allValues[rowIndex] as (string | number | boolean)[]);
  }

  usedRange.clear(ExcelScript.ClearApplyTo.contents);

  const outputRange = worksheet.getRangeByIndexes(
    headerRowIndex, // write starting at header row
    0,
    outputValues.length,
    totalColCount
  );
  outputRange.setValues(outputValues);
}

function toComparableTime(value: string | number | boolean): number {
  if (typeof value === "number" && Number.isFinite(value)) {
    return value; // Excel serial date or numeric timestamp-like value
  }

  if (typeof value === "string") {
    const parsed = Date.parse(value);
    return Number.isFinite(parsed) ? parsed : Number.NEGATIVE_INFINITY;
  }

  return Number.NEGATIVE_INFINITY;
}

© 2026 Better Solutions Limited. All Rights Reserved. © 2026 Better Solutions Limited TopPrevNext