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 powerful bulk update feature to your Knack table element. It allows you to:

  • Select multiple tasks using checkboxes
  • Update all selected tasks' status at once
  • Save time by avoiding individual edits
  • Works seamlessly with filters, sorting, and pagination

Important: Next-Gen vs Classic Knack Apps

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

Before You Start - What You'll Need

1. Find Your View Key

  • Go to your Knack Builder
  • Navigate to the page with your table
  • Click on the table view
  • Look for the view key in the settings (it looks like view_XX)
  • Write this down - you'll need it!

2. Find Your Application ID

  • In Knack Builder, go to Settings → API & Code
  • Copy your Application ID (looks like: 68bf8eaffe4a5002932f429a)
  • Keep this handy

3. Find Your Scene Key

  • Navigate to the page containing your table in Knack Builder
  • Look at the page URL or inspect the page source
  • Find the scene key (looks like scene_XX)
  • Note this down

4. Find Your Status Field Key

  • In your table's data source (object), find your status field
  • Click on the field to see its key (looks like field_XX)
  • Note this down

The Complete Script with Explanations

// This tells Knack to wait until the page is fully loaded before running our code
Knack.ready().then(async () => {

  // 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', 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
    
    // ===== CONFIGURATION - UPDATE THESE VALUES =====
    
    // CUSTOMIZE THIS: Replace with your Application ID from Knack Settings
    const appId = 'YOUR_APP_ID';
    
    // CUSTOMIZE THIS: Replace with your scene key
    const sceneKey = 'YOUR_SCENE_KEY';
    
    // CUSTOMIZE THIS: Replace with your status field key
    const statusFieldKey = 'YOUR_FIELD_KEY';

    // CUSTOMIZE THIS: These are your status options
    // Change the values and labels to match your workflow
    const statusOptions = [
      { value: 'New', label: 'New' },
      { value: 'In Progress', label: 'In Progress' },
      { value: 'Review', label: 'Review' },
      { value: 'Complete', label: 'Complete' }
    ];

    // 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 TABLE =====

      // Add "Select All" checkbox to the table header
      const headerRow = table.querySelector('thead tr');
      if (headerRow) {
        const selectAllTh = document.createElement('th');
        selectAllTh.style.width = '50px';
        selectAllTh.style.textAlign = 'center';
        selectAllTh.innerHTML = '<input type="checkbox" id="select-all-tasks">';
        headerRow.insertBefore(selectAllTh, headerRow.firstChild);  // Add as first column
      }

      // Add individual checkboxes to each row
      const bodyRows = table.querySelectorAll('tbody tr');
      bodyRows.forEach(function (row, index) {
        // Make sure this row has a record with an ID
        if (records[index] && records[index].id) {
          const recordId = records[index].id;  // Get the record's unique ID
          
          // Create a table cell with a checkbox
          const checkboxTd = document.createElement('td');
          checkboxTd.style.textAlign = 'center';
          checkboxTd.style.verticalAlign = 'middle';
          // Store the record ID with the checkbox so we know what to update
          checkboxTd.innerHTML = '<input type="checkbox" class="task-checkbox" data-record-id="' + recordId + '">';
          row.insertBefore(checkboxTd, row.firstChild);  // Add as first column
        }
      });

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

      // ===== 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.value;      // What gets saved
          option.textContent = status.label; // 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

  });  // End of Knack.on

});  // End of Knack.ready

How to Customize This Script

Step 1: Update the View Key

Find this line near the top:

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

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

Step 2: Update the Application ID

Find this line:

const appId = 'YOUR_APP_ID';

Replace YOUR_APP_ID with your Application ID from Knack Settings (e.g., 68bf8eaffe4a5002932f429a).

Step 3: Update the Scene Key

Find this line:

const sceneKey = 'YOUR_SCENE_KEY';

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

Step 4: Update the Field Key

Find this line:

const statusFieldKey = 'YOUR_FIELD_KEY';

Replace YOUR_FIELD_KEY with your actual status field key (e.g., field_36).

Step 5: Update Status Options

Find this section:

const statusOptions = [
  { value: 'New', label: 'New' },
  { value: 'In Progress', label: 'In Progress' },
  { value: 'Review', label: 'Review' },
  { value: 'Complete', label: 'Complete' }
];

Change these to match your actual status values. The value is what gets saved to the database, and label is what users see in the dropdown.

Testing Your Script

  1. Start Small: Test with just one or two 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 fixed in the updated code with the cleanup section. Make sure you're using the latest version with the cleanup code (lines 53-63).

"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

  • Double-check your Application ID is correct.
  • Verify your scene key, view key, and field key are correct.
  • Make sure the status values match exactly (including capitalization).
  • 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 updated 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. No jQuery: Next-Gen doesn't include jQuery, so we use native fetch() instead of $.ajax().
  3. No Router: Knack.router doesn't exist, so scene keys must be hard-coded.
  4. Cleanup Required: The cleanup section is critical to prevent duplicate buttons when filters are applied.

Safety Tips

  • Always backup your data before testing bulk operations.
  • Test in a development environment first if possible.
  • Start with a small batch to make sure it works correctly.
  • Keep the original code saved somewhere safe.
  • 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. Verify you're using a Next-Gen Knack app (check version in bottom-right corner).
  6. Ensure users are logged in before attempting bulk updates.