Accessing Knack.api via custom JavaScript

So I am attempting to retrieve sessions that are set as “Auto” (in the Register Type column of my Sessions Table) so that they automatically appear on a persons customized conference agenda without the user having to take any specific action other than logging in. There are sessions they have to register for and those are labled in the column Register Type as “Manual.” Being fully transparent I am attempting to figure this out with Gemini as I am not a JavaScript master on any level. What am I doing wrong? I ensured the object and field types are correct.

My objective is to automatically create records in a ‘Session Attendance’ object (object_21 ) whenever a user is logged in, for any session in my ‘Sessions’ object (object_3 ) that has a specific field (Register Type , field_188 ) set to the value “Auto”. This should happen automatically in the background.

Application Details:

  • App URL: https://erau.knack.com/summit2025
  • Access Method: The application is accessed directly via its URL (it is not embedded in another website/iframe).
  • Authentication: We use SAML SSO (via Okta) for user login.

The Problem: I have been attempting to implement this using custom JavaScript attached to standard Knack events (knack-page-render.any and knack-scene-render.any ). However, the script consistently fails because the Knack.Api object appears to be undefined at the time the script executes, even when including setTimeout delays.

Diagnostic console logs confirm that the main Knack object is defined (type object ), but attempting to access Knack.Api results in undefined . This prevents any API calls (like Knack.Api.Model.fetchAll or Knack.Api.Model.create ) from succeeding.

Code Attempted: Here is the latest version of the code we tried, using the knack-scene-render.any event and diagnostic checks:

// --- Knack JavaScript for Automatic Session Registration ---
// Version 5: Switched to knack-scene-render.any event listener

$(document).on('knack-scene-render.any', function(event, scene) { // *** Using knack-scene-render.any ***

    // --- Configuration ---
    const SESSION_OBJECT_ID = 'object_3';         // Sessions Object ID
    const REGISTER_TYPE_FIELD = 'field_188';      // 'Register Type' field ID on Sessions table
    const SESSION_ATTENDANCE_OBJECT_ID = 'object_21'; // Session Attendance Object ID
    const SESSION_FIELD_ON_ATTENDANCE = 'field_149'; // Connection to Session on Attendance table
    const USER_FIELD_ON_ATTENDANCE = 'field_156';   // Connection to User on Attendance table
    // --- End Configuration ---

    const checkFlag = 'autoSessionsChecked_v1_' + SESSION_OBJECT_ID;
    if (sessionStorage.getItem(checkFlag)) {
        return;
    }

    setTimeout(() => {
        const loggedInUser = Knack.getUserAttributes();
        if (!loggedInUser || !loggedInUser.id || loggedInUser.id === 'Unknown') {
            return;
        }
        const userId = loggedInUser.id;

        if (sessionStorage.getItem(checkFlag)) {
            return;
        }

        console.log(`DEBUG: 1. Running check inside 'knack-scene-render.any' for user: ${userId} (scene: ${scene.key})`);

        // Diagnostic Checks
        console.log(`DEBUG: Checking Knack object type: ${typeof Knack}`);
        if (typeof Knack !== 'undefined') {
             console.log(`DEBUG: Checking Knack.Api object type: ${typeof Knack.Api}`);
             if (typeof Knack.Api !== 'undefined') {
                  console.log(`DEBUG: Checking Knack.Api.Model object type: ${typeof Knack.Api.Model}`);
             }
        }

        // Abort if API is not available
        if (typeof Knack === 'undefined' || typeof Knack.Api === 'undefined' || typeof Knack.Api.Model === 'undefined') {
             console.error("DEBUG: ERROR - Knack API components still undefined even in knack-scene-render. Aborting.");
             sessionStorage.setItem(checkFlag, 'true'); // Prevent loops
             return;
        }

        console.log("DEBUG: 1a. Knack API seems available. Attempting to fetch Auto sessions...");

        let autoSessionIds = [];
        let userRegisteredSessionIds = new Set();

        // Fetch Auto Sessions
        Knack.Api.Model.fetchAll({
            model: SESSION_OBJECT_ID,
            filters: [{ field: REGISTER_TYPE_FIELD, operator: 'is', value: 'Auto' }],
            auth: 'user'
        })
        .then(function(autoSessionsResult) {
            // Process results and fetch existing attendance... (full logic as provided previously)
             if (!autoSessionsResult || !autoSessionsResult.records) { throw new Error("Failed to fetch Auto sessions."); }
            autoSessionIds = autoSessionsResult.records.map(record => record.id);
            console.log(`DEBUG: 2. Found ${autoSessionIds.length} Auto Session IDs:`, autoSessionIds);
            if (autoSessionIds.length === 0) { return { records: [] }; }
            console.log("DEBUG: 3. Fetching existing attendance records for user...");
            return Knack.Api.Model.fetchAll({ model: SESSION_ATTENDANCE_OBJECT_ID, filters: [{ field: USER_FIELD_ON_ATTENDANCE, operator: 'is', value: userId }], auth: 'user' });
        })
        .then(function(attendanceResult) {
            // Process attendance and determine needed registrations... (full logic as provided previously)
             if (!attendanceResult || !attendanceResult.hasOwnProperty('records')) { throw new Error("Failed fetching attendance."); }
            console.log(`DEBUG: 4. Fetched ${attendanceResult.records.length} existing attendance records.`);
            attendanceResult.records.forEach(record => { const conn = record[SESSION_FIELD_ON_ATTENDANCE + '_raw']; if (conn && conn.length > 0) { userRegisteredSessionIds.add(conn[0].id); } });
            console.log('DEBUG: 5. User registered for IDs:', Array.from(userRegisteredSessionIds));
            const sessionsToRegister = autoSessionIds.filter(id => !userRegisteredSessionIds.has(id));
            console.log('DEBUG: 6. Sessions needing registration:', sessionsToRegister);
            if (sessionsToRegister.length === 0) { return []; }
            console.log(`DEBUG: 7. Attempting to create ${sessionsToRegister.length} records...`);
            // Create missing records... (full logic as provided previously)
            const createPromises = sessionsToRegister.map(sessionId => { const d = {}; d[USER_FIELD_ON_ATTENDANCE] = userId; d[SESSION_FIELD_ON_ATTENDANCE] = sessionId; return Knack.Api.Model.create({ model: SESSION_ATTENDANCE_OBJECT_ID, data: d, auth: 'user' }); });
            return Promise.all(createPromises);
        })
        .then(function(creationResults) {
            // Final logging... (full logic as provided previously)
             console.log('DEBUG: 8. Processing creation results.');
             if (creationResults.length > 0) { console.log(`DEBUG: 8a. Successfully created ${creationResults.length} records.`); } else { console.log('DEBUG: 8b. No new records created.'); }
             sessionStorage.setItem(checkFlag, 'true');
             console.log('DEBUG: 9. Check complete.');
        })
        .catch(function(error) {
            console.error('DEBUG: FINAL ERROR during process:', error);
        });

    }, 300);
});

Console Output Evidence: The console logs consistently show DEBUG: Checking Knack.Api object type: undefined , followed by the error message indicating the script is aborting because the API components are undefined. I can provide screenshots of this console output if needed.

Michael

I’m no expert on Knack APIs, but I think Gemini is misleading you here - there is no “Knack.Api” object as far as I know - this looks like a “hallucination” by Gemini trying to guess what it might be. There are no fetchall and create APIs in Knack. All Knack offers in the API space is GET, POST and PUT.

I will leave it to the API gurus to advise you how you might go about doing what you are trying to do. And my advice is to use view based APIs otherwise you will be exposing the API credentials to the world - very insecure.

You might also be able to achieve what you are looking for using Flows, but again that isn’t my strength yet.

Michael

Ok, I wiped out my previous “this is what ChatGPT recommends” because it didn’t work, so I spent a few hours learning from an AI today and have something that does work that maybe you can adapt to your needs.

I set up a sandbox with 2 tables - COURSES and ASSIGNMENTS.

COURSES has the name of the course, and whether it is AUTO or MANUAL assignment (in this case its a selection)

ASSIGNMENTS has just 2 fields: the Account (connection) and the COURSE (connection)

I have a page that has 3 views:

COURSES, a grid filtered to only show AUTO assigned courses. (view_98)

ASSIGNMENTS, a grid filtered to show all courses assigned to the logged in user (view_103)

ADD ASSIGNMENT - a form that adds an entry to the ASSIGNMENT table, connected to the logged in user. (view_104)

Looks like this:

Then I have some CSS that hides these views and makes them invisible. The idea would be that these 3 views are added to the bottom of whatever your HOME PAGE is, so that they will be rendered as soon as the person logs in. But due to the CSS, no-one will see them. Just the javascript.

CSS:

#view_98,
#view_103,
#view_104 {
  position: absolute !important;
  left: -9999px !important;
  top: -9999px !important;
  visibility: hidden !important;
  height: 1px !important;
  width: 1px !important;
  overflow: hidden !important;
}

Ok, now here is where the automatic assignment of “AUTO” courses happens. The following javascript will run whenever the views are rendered - so if they are tacked onto the home page that means whenever the home page is rendered. Basically what it does is check to see if there are any auto assigned courses, then checks to see is they are already assigned to the logged in user, and if not it assigns them. Exactly how it does this is still a bit of a mystery to me, but it took a couple of hours messing with ChatGPT to get something that worked. Maybe someone who knows what they are doing with javascript can simplify it, but it works and that was good enough for me.

Here is the javascript. You will need to change the views to match whatever you have - using the above view numbers as a guide. You will need to change the field numbers to match yours - the comments tell you what the fields are for. Reach out if your cant work it out - I can possibly help answer specific questions.

But the net result of this is that any course that is marked as AUTO assigned, will be assigned to the user when they log in. Which is sort of what I understood you are tryin to achieve. NO APIs needed.

// -------------------------------
// Auto Assignment via Knack Form
// -------------------------------

let autoSessions = [];
let assignedSessionIds = [];
let autoLoaded = false;
let attendanceLoaded = false;
let isAssigning = false;
const MAX_ATTEMPTS = 5;
const RETRY_INTERVAL = 300; // milliseconds

// View 98: List of auto sessions
$(document).on('knack-view-render.view_98', function (event, view, data) {
  autoSessions = data.map(r => ({
    id: r.id,
    name: r.field_161 // The session name
  }));
  autoLoaded = true;
  tryCreateAssignments();
});

// View 103: Already assigned sessions (via field_164)
$(document).on('knack-view-render.view_103', function (event, view, data) {
  assignedSessionIds = data.flatMap(r => 
    Array.isArray(r.field_164_raw) ? r.field_164_raw.map(s => s.id) : []
  );
  attendanceLoaded = true;
  tryCreateAssignments();
});

// Check readiness and initiate assignments
function tryCreateAssignments() {
  if (!autoLoaded || !attendanceLoaded || isAssigning) return;

  const sessionsToAssign = autoSessions.filter(
    s => !assignedSessionIds.includes(s.id)
  );

  if (!sessionsToAssign.length) return;

  isAssigning = true;

  sessionsToAssign.forEach((session, index) => {
    setTimeout(() => {
      waitForDropdownAndSubmit(session, 0);
    }, index * 1000);
  });

  // Reset state after all sessions are queued
  setTimeout(() => {
    autoSessions = [];
    assignedSessionIds = [];
    autoLoaded = false;
    attendanceLoaded = false;
    isAssigning = false;
  }, sessionsToAssign.length * 1000 + 500);
}

// Wait for dropdown to populate, then submit form for given session
function waitForDropdownAndSubmit(session, attempt) {
  const $form = $('#view_104 form');
  const $dropdown = $('#view_104 select[name="field_164"]');

  if ($form.length && $dropdown.length) {
    const option = $dropdown.find(`option[value="${session.id}"]`);
    if (option.length) {
      $dropdown.val(session.id);
      $form.submit();
    } else if (attempt < MAX_ATTEMPTS) {
      setTimeout(() => waitForDropdownAndSubmit(session, attempt + 1), RETRY_INTERVAL);
    }
  } else if (attempt < MAX_ATTEMPTS) {
    setTimeout(() => waitForDropdownAndSubmit(session, attempt + 1), RETRY_INTERVAL);
  }
}

Best of luck!

2 Likes

Thank you for this, I managed to find a workaround firing off JS. Interesting thing is AI proposed an approach that would have made sense but Knack Flows are simply not yet that developed out to do what it suggested by using the iterator tool within flows but to accomplish what I needed would have required a nested interator which is not supported.

1 Like