Bulk Update Status for Knack Tables

Learn how to add bulk update functionality to your Knack table elements with this comprehensive JavaScript implementation guide. This script enables users to select multiple records using checkboxes and update their status fields simultaneously, dramatically improving workflow efficiency. Includes complete code with detailed explanations, step-by-step customization instructions, and troubleshooting tips for common issues.

What This Script Does

This script adds a bulk update feature to your Knack table element, in the Live App. Rather than editing records one at a time, you can select multiple rows using checkboxes and update all of their status values in a single action.

A common use case is a task or project tracker. Imagine you have a table of tasks, each with a Status field that can be set to values like New, In Progress, Review, or Complete. At the end of a sprint, instead of opening each task individually to mark it complete, you can check them all off and update them in one click. The same pattern works for any Multiple Choice field in your app — order statuses, application stages, approval states, and so on.

The script:

  • Adds checkboxes to each row in your Live App table
  • Places a Bulk Update Status button above the table
  • Opens a popup when clicked, letting you choose a new status value
  • Updates all selected records via the Knack API and refreshes the page
👍

Inline Editing MUST be enabled: The Knack API only allows view-based PUT requests if the view itself supports editing. You must go to your table view settings in the Page Editor and enable Inline Editing for the table.

Important: Next-Gen vs Classic Knack Apps

This guide is for Next-Gen Knack Apps . If you're using a Classic Knack app, the code will be different.

Before You Start - What You'll Need

You'll need to gather a few keys from your Knack Builder before adding the script. All of these are found in the Builder.

1. Find Your View Key

  • Go to the Page Editor
  • Navigate to the page with your table
  • Click on the Page Settings, click Show Element Keys
  • Look for the view key in the settings (it looks like view_XX)
  • Write this down

2. Find Your Scene Key

  • Go to the Page Editor

  • Navigate to the page with your table

  • Click on the Page Settings

  • Find the scene key (looks like scene_XX)

  • Note this down


3. Find Your Status Field Key and Object Key

This script targets a Multiple Choice field in your table — the field whose value you want to bulk update. In the example above, that's the Status field, but it can be any Multiple Choice field in your app.

To find the keys you need:

  • Go to the datatable and select the Fields tab
  • Find the field to see its key (looks like field_XX) — this is your statusFieldKey

The datatable itself also has a key (looks like object_XX) — this is your statusFieldObjectKey. It is in the browser url while you are on the datatable.


👍

The field MUST be in the view: The Multiple Choice field you are trying to update (e.g., Status) must be present as a column in the table in the Live App.

The script uses these two keys to automatically fetch your field's existing Multiple Choice options at runtime, so you don't need to hardcode them manually. Whatever options you've configured on that field in the Builder will appear in the bulk update dropdown automatically.

Tip: You can use this script with any Multiple Choice field, not just one named "Status". Just point statusFieldKey and statusFieldObjectKey at whichever field you want to bulk update.

The Complete Script with Explanations

// CUSTOMIZE THIS: Replace 'view_XX' with your table view key
// This listens for when your specific table loads on the page
Knack.on('records:render:view_XX', async function (event) {

  // These grab the data from your table
  const records = event.records;  // All the records in your table
  const viewKey = event.viewKey;   // The view identifier
  const appId = (await Knack.getApplicationDetails())?.id; // Your application ID (fetched automatically)

  // ===== CONFIGURATION - UPDATE THESE VALUES =====
  const sceneKey = 'scene_XX'; // Replace with your scene key
  const statusFieldKey = 'field_XX'; // Replace with your status field key
  const statusFieldObjectKey = 'object_XX'; // Replace with your status field's object key

  // Fetches your field's Multiple Choice options automatically from the Builder configuration
  const fieldDef = await Knack.getField(statusFieldKey, statusFieldObjectKey);
  const rawOptions = fieldDef?.format?.options || [];
  // Handles both plain string arrays and object arrays (e.g. { val: 'New' })
  const statusOptions = rawOptions.map(function (opt) {
    return (typeof opt === 'object' && opt !== null) ? (opt.val || opt.label || String(opt)) : String(opt);
  });

  // Wait half a second for the table to fully render
  // (Sometimes Knack needs a moment to build the table)
  setTimeout(function () {

    // Find the container that holds your table view
    const viewContainer = document.getElementById(viewKey);
    if (!viewContainer) return;  // Stop if we can't find it

    // Find the actual table inside the container
    const table = viewContainer.querySelector('table');
    if (!table) return;  // Stop if there's no table

    // ===== CLEANUP: Remove existing elements to prevent duplicates =====
    // This is important! It prevents duplicate buttons when filters are applied

    document.querySelectorAll('#bulk-status-btn').forEach(function (btn) {
      if (btn && btn.parentNode) btn.parentNode.remove();
    });

    document.querySelectorAll('#select-all-tasks').forEach(function (checkbox) {
      if (checkbox && checkbox.parentNode) checkbox.parentNode.remove();
    });

    document.querySelectorAll('.task-checkbox').forEach(function (checkbox) {
      if (checkbox && checkbox.parentNode) checkbox.parentNode.remove();
    });

    // ===== CREATE THE BULK UPDATE BUTTON =====

    // Create a container for our button
    const buttonDiv = document.createElement('div');
    buttonDiv.style.marginTop = '15px';     // Space above
    buttonDiv.style.marginBottom = '20px';  // Space below
    buttonDiv.style.paddingLeft = '16px';   // Align with table

    // Create the actual button
    const bulkButton = document.createElement('button');
    bulkButton.id = 'bulk-status-btn';
    bulkButton.textContent = 'Bulk Update Status';  // Button text

    // Style the button to look nice
    bulkButton.style.backgroundColor = '#3b82f6';  // Blue color
    bulkButton.style.color = 'white';              // White text
    bulkButton.style.padding = '10px 20px';        // Internal spacing
    bulkButton.style.border = 'none';              // No border
    bulkButton.style.borderRadius = '6px';         // Rounded corners
    bulkButton.style.fontSize = '14px';            // Text size
    bulkButton.style.fontWeight = '500';           // Medium bold
    bulkButton.style.cursor = 'pointer';           // Hand cursor on hover

    // Make button darker on hover (visual feedback)
    bulkButton.addEventListener('mouseenter', function () {
      bulkButton.style.backgroundColor = '#2563eb';  // Darker blue
    });
    bulkButton.addEventListener('mouseleave', function () {
      bulkButton.style.backgroundColor = '#3b82f6';  // Back to normal
    });

    // Add the button to its container, then add container above the table
    buttonDiv.appendChild(bulkButton);
    table.parentNode.insertBefore(buttonDiv, table);

    // Add checkboxes to the grid
    addCheckboxesToGrid(viewKey, true);

    // ===== HANDLE BULK UPDATE BUTTON CLICK =====

    bulkButton.addEventListener('click', async function () {

      // Collect all selected record IDs (scoped to current view)
      const selectedTasks = [];
      const checkedBoxes = viewContainer.querySelectorAll('.task-checkbox:checked');

      checkedBoxes.forEach(function (checkbox) {
        const recordId = checkbox.getAttribute('data-record-id');
        if (recordId) {
          selectedTasks.push(recordId);  // Add to our list
        }
      });

      // Make sure at least one task is selected
      if (selectedTasks.length === 0) {
        alert('Please select at least one task.');
        return;  // Stop here
      }

      // ===== CREATE THE POPUP MODAL =====

      // Create dark overlay that covers the whole screen
      const modal = document.createElement('div');
      modal.style.position = 'fixed';
      modal.style.top = '0';
      modal.style.left = '0';
      modal.style.width = '100%';
      modal.style.height = '100%';
      modal.style.backgroundColor = 'rgba(0,0,0,0.5)';  // Semi-transparent black
      modal.style.display = 'flex';
      modal.style.alignItems = 'center';       // Center vertically
      modal.style.justifyContent = 'center';   // Center horizontally
      modal.style.zIndex = '9999';            // Put on top of everything

      // Create the white popup box
      const modalContent = document.createElement('div');
      modalContent.style.backgroundColor = 'white';
      modalContent.style.padding = '30px';
      modalContent.style.borderRadius = '8px';  // Rounded corners
      modalContent.style.maxWidth = '400px';
      modalContent.style.width = '90%';

      // Add heading showing how many tasks will be updated
      const heading = document.createElement('h3');
      heading.style.marginTop = '0';
      heading.textContent = 'Update ' + selectedTasks.length + ' tasks';

      // Create dropdown for status selection
      const selectElement = document.createElement('select');
      selectElement.id = 'status-select';
      selectElement.style.width = '100%';
      selectElement.style.padding = '10px';
      selectElement.style.marginBottom = '20px';
      selectElement.style.fontSize = '16px';

      // Add default "choose one" option
      const defaultOption = document.createElement('option');
      defaultOption.value = '';
      defaultOption.textContent = '-- Select Status --';
      selectElement.appendChild(defaultOption);

      // Add all your status options to the dropdown
      statusOptions.forEach(function (status) {
        const option = document.createElement('option');
        option.value = status;      // What gets saved
        option.textContent = status; // What user sees
        selectElement.appendChild(option);
      });

      // Create container for buttons
      const buttonContainer = document.createElement('div');
      buttonContainer.style.textAlign = 'right';

      // Create Cancel button
      const cancelBtn = document.createElement('button');
      cancelBtn.textContent = 'Cancel';
      cancelBtn.style.marginRight = '10px';
      cancelBtn.style.padding = '8px 16px';
      cancelBtn.style.cursor = 'pointer';

      // Create Update button
      const updateBtn = document.createElement('button');
      updateBtn.textContent = 'Update';
      updateBtn.style.padding = '8px 16px';
      updateBtn.style.cursor = 'pointer';

      // Put everything together
      buttonContainer.appendChild(cancelBtn);
      buttonContainer.appendChild(updateBtn);
      modalContent.appendChild(heading);
      modalContent.appendChild(selectElement);
      modalContent.appendChild(buttonContainer);
      modal.appendChild(modalContent);
      document.body.appendChild(modal);  // Show the modal

      // Handle Cancel button - just close the modal
      cancelBtn.addEventListener('click', function () {
        document.body.removeChild(modal);  // Remove the popup
      });

      // Handle Update button - this does the actual work
      updateBtn.addEventListener('click', async function () {
        const selectedStatus = selectElement.value;  // Get chosen status value
        const selectedStatusLabel = selectElement.options[selectElement.selectedIndex].text;  // Get display text

        // Make sure a status was selected
        if (!selectedStatus) {
          alert('Please select a status.');
          return;
        }

        // Close the modal
        document.body.removeChild(modal);

        // ===== GET USER AUTHENTICATION TOKEN =====
        // This is required for Next-Gen Knack apps
        let userToken = null;
        try {
          const user = await Knack.getUser();
          if (user && user.token) {
            userToken = user.token;
          }
        } catch (e) {
          console.error('Could not get user token:', e);
        }

        // Track progress
        let updateCount = 0;    // How many succeeded
        let errorCount = 0;     // How many failed
        const totalTasks = selectedTasks.length;

        // ===== UPDATE EACH SELECTED RECORD =====

        selectedTasks.forEach(function (recordId) {
          // Create the update data object
          const updateData = {};
          updateData[statusFieldKey] = selectedStatus;  // The field to update and its new value

          // Build the API URL
          const apiUrl = 'https://api.knack.com/v1/pages/' + sceneKey + '/views/' + viewKey + '/records/' + recordId;

          // Set up headers with authentication
          const headers = {
            'X-Knack-Application-Id': appId,  // Your app ID
            'Content-Type': 'application/json'  // We're sending JSON data
          };

          // Add user token for authentication
          if (userToken) {
            headers['Authorization'] = userToken;
          }

          // Make the API call to update this record
          fetch(apiUrl, {
            method: 'PUT',  // PUT means "update"
            headers: headers,
            body: JSON.stringify(updateData)  // Convert our data to JSON format
          })
            .then(function (response) {
              if (!response.ok) {
                return response.text().then(function (text) {
                  throw new Error('Update failed: ' + text);
                });
              }
              return response.json();
            })
            .then(function (data) {
              updateCount++;  // Success! Increment counter

              // Check if we're done with all updates
              if (updateCount + errorCount === totalTasks) {
                if (errorCount > 0) {
                  // Some failed - show both counts
                  alert(updateCount + ' updated, ' + errorCount + ' failed');
                } else {
                  // All succeeded!
                  alert(totalTasks + ' tasks updated to ' + selectedStatusLabel);
                }
                location.reload();  // Refresh the page to show updates
              }
            })
            .catch(function (error) {
              console.error('Error updating record ' + recordId + ':', error);
              errorCount++;  // This one failed

              // Check if we're done with all updates
              if (updateCount + errorCount === totalTasks) {
                alert(updateCount + ' updated, ' + errorCount + ' failed');
                location.reload();  // Refresh the page
              }
            });
        });
      });
    });

  }, 500);  // End of setTimeout

  // Purpose: Add checkboxes to a grid
  function addCheckboxesToGrid(viewKey, selectAll = true) {

    const viewContainer = document.getElementById(viewKey);
    const table = viewContainer.querySelector('table');
    if (!table) return;  // Stop if there's no table

    // Add individual checkboxes to each row
    const bodyRows = table.querySelectorAll('tbody tr');
    bodyRows.forEach(function (row, index) {
      if (records[index] && records[index].id) { // Make sure this row has a record with an ID
        const recordId = records[index].id; // Get the record's unique ID
        const checkboxTd = row.querySelector('td:first-child').cloneNode(true); // Create a table cell with a checkbox
        Object.assign(checkboxTd.style, { display: 'flex', alignItems: 'center', justifyContent: 'center', width: 'unset' }); // Apply custom CSS
        checkboxTd.innerHTML = '<input type="checkbox" class="task-checkbox" data-record-id="' + recordId + '">'; // Store the record ID with the checkbox so we know what to update
        row.insertBefore(checkboxTd, row.firstChild);  // Add as first column
      }
    });

    const headerRow = table.querySelector('thead tr');
    if (headerRow) {
      const selectAllTh = document.querySelector('thead tr th:first-child').cloneNode(true);
      Object.assign(selectAllTh.style, { display: 'flex', alignItems: 'center', justifyContent: 'center', width: 'unset' }); // Apply custom CSS
      headerRow.insertBefore(selectAllTh, headerRow.firstChild);  // Add as first column

      const selectAllCheckbox = Object.assign(document.createElement('input'), { type: 'checkbox', id: 'select-all-tasks' });
      selectAllTh.replaceChildren(selectAllCheckbox); // Add checkbox to table header cell
      if (!selectAll) {
        selectAllCheckbox.style.visibility = "hidden"; // Hide the select all checkbox if not enabled
        return; // Exit if select all is not enabled
      }

      selectAllCheckbox.addEventListener('change', function () { // Add select all function
        const checkboxes = viewContainer.querySelectorAll('.task-checkbox'); // Find all individual checkboxes in THIS view only (important for filters!)
        checkboxes.forEach(function (checkbox) { // Check/uncheck them all based on "Select All" state
          checkbox.checked = selectAllCheckbox.checked;
        });
      });
    }

  }

});  // End of Knack.on

How to Customize This Script

Step 1: Update the View Key

Find this line near the top:

Knack.on('records:render:view_XX', async function (event) {

Replace view_XX with your actual view key (e.g., view_37).

Step 2: Update the Scene Key

Find this line:

const sceneKey = 'scene_XX'; // Replace with your scene key

Replace scene_XX with your scene key (e.g., scene_33).

Step 3: Update the Field Key and Object Key

Find these two lines:

const statusFieldKey = 'field_XX'; // Replace with your status field key
const statusFieldObjectKey = 'object_XX'; // Replace with your status field's object key

Replace field_XX with your Multiple Choice field key (e.g., field_36) and object_XX with the key of the object it belongs to (e.g., object_5).

Once you've set these two values, the script will automatically fetch the field's options from your Builder configuration and populate the dropdown in the bulk update popup. You manage the available status values directly in the Builder — just edit the Multiple Choice options on that field, and the script will reflect your changes automatically without any code updates.

Note: Your Application ID is also fetched automatically — you don't need to find or hardcode it.

Testing Your Script

  1. Start Small: Test with just one or two test records first.
  2. Check Console: Open browser Developer Tools (F12) to see any error messages.
  3. Verify Updates: After running, check that your records actually updated.
  4. Test with Filters: Apply a filter and make sure only visible records are selected.
  5. Test Edge Cases: Try with no selection, try canceling, try "Select All", etc.

Troubleshooting Common Issues

Duplicate Buttons Appear When Filtering

Solution: This is handled in the code with the cleanup section. Make sure you're using the provided version with the cleanup code (lines 31-41).

"Please select at least one task" Alert

Cause: No checkboxes are selected.

Solution: Click checkboxes to select records before clicking the update button.

Updates Aren't Working

  • Verify your scene key, view key, field key, and object key are correct.
  • Ensure the user is logged in (required for Next-Gen apps).
  • Check browser console for specific error messages.

Checkboxes Don't Appear

  • The script might be running too fast - try increasing the timeout from 500 to 1000.
  • Check that your table has records.
  • Verify your view key is correct in the event listener.

Some Updates Fail

  • This usually means those records are locked or have validation rules.
  • Check if those records have required fields that aren't being updated.
  • Verify the user has permission to edit those records.

Wrong Number of Records Selected

Cause: In older versions, this happened when filters were applied.

Solution: The code uses viewContainer.querySelectorAll() instead of document.querySelectorAll() to scope selection to only visible records.

Key Differences from Classic Knack Apps

If you're migrating from Classic Knack, note these important changes:

  1. Authentication: Next-Gen uses await Knack.getUser() to get the user token, not Knack.getUserToken().
  2. Dynamic Data Fetching: We use native async methods like await Knack.getApplicationDetails() and await Knack.getField() instead of hardcoding options.
  3. No jQuery: Next-Gen doesn't include jQuery, so we use native fetch() instead of $.ajax().
  4. No Router: Knack.router doesn't exist, so scene keys must be hard-coded.
  5. Cleanup Required: The cleanup section is critical to prevent duplicate buttons when filters are applied.

Safety Tips

  • Test in a sample table first
  • Start with a small batch to make sure it works correctly.
  • Users must be logged in for this to work (it uses their session token).

Need More Help?

If something isn't working:

  1. Check all your keys and IDs are correct.
  2. Look at the browser console (F12) for error messages.
  3. Try updating just one record manually first to ensure it's possible.
  4. Make sure you have permission to update these records in Knack.
  5. Ensure inline editing is enabled for the field.
  6. Ensure users are logged in before attempting bulk updates.

https://www.tella.tv/video/vid_cmn9hxpxu01i504l4fbu68wzm/embed?b=0&title=0&a=1&loop=0&autoPlay=true&t=0&muted=1&wt=0&o=0