How to Create Template Tasks and Auto-Generate Them in Knack

Read this guide if you want to:

  1. Create reusable template tasks

  2. Reduce your automation costs

  3. Build efficient internal tools for renovation, service, or construction workflows


:waving_hand: Hey Knack Geeks!

After exploring construction platforms like Procore and Buildertrend, one standout feature became obvious — task templates.
Templates save hours of repetitive setup by letting you clone predefined tasks or schedules that follow a predictable pattern.

Let’s walk through how to recreate this feature inside Knack — efficiently and affordably.


:building_construction: Why Templates Matter

In construction, most projects repeat the same set of steps.
For instance, if you run a roofing company, your tasks might look like this:

Normal Roof Inspection

  1. Set up inspection

  2. Visit the property

  3. Check shingles

  4. Identify water intrusion

  5. Send invoice

Storm Damage Inspection

  1. Contact insurance

  2. Get liability coverage

  3. Set up inspection

  4. Check shingles

  5. Identify water intrusion

Rather than rebuilding these tasks every time, we can use Knack and JavaScript to clone task templates automatically.


:brick: Data Model Setup

Here are the objects and relationships you’ll need:

Objects:

  1. Project – contains primary information

  2. Project Type – groups your templates (e.g., Roofing, Renovation)

  3. Task Template – predefined tasks for each project type

  4. Task – the cloned, real tasks linked to an active project

Relationships:

  • A Project belongs to a Project Type

  • A Task Template belongs to a Project Type

  • A Task links to both a Project and a Project Type


:gear: Choosing the Right Automation Approach

Here’s a quick comparison of possible methods:

Method Feasibility Notes
Knack Webpage Trigger :cross_mark: Not feasible Requires manual entry for each new task
Knack Task :cross_mark: Not feasible Still too much admin work
Knack Flow :warning: Possible But will quickly burn through your monthly flow quota
Make / Zapier :warning: Possible Also costly for high-volume operations
JavaScript + API :white_check_mark: Best option Fast, low-cost, and highly flexible

:light_bulb: Why Code Instead of Flow?

Because cost and control matter.

Knack Pro accounts support 3,000 API calls per day,
while Flows are limited to 2,000 transactions per month.

So coding isn’t just fun — it’s efficient.


:puzzle_piece: Key Challenge: Hidden Primary and Foreign Keys

Knack doesn’t expose Primary Keys (PK) or Foreign Keys (FK) directly in the UI.
You’ll need to access them through the API or JavaScript console.

To get the Project’s Primary Key:

const view = Knack.views['view_42'];
const projectId =
  view?.model?.attributes?.id ||
  view?.model?.data?.id ||
  (window.location.hash.match(/\/([a-f0-9]{24})(?:\/|$)/i) || [])[1];

To get the Project Type (connecting record):

const getProjectUrl = `https://api.knack.com/v1/objects/object_26/records/${projectId}`;
const getProject = await fetch(getProjectUrl, {
  headers: {
    'X-Knack-Application-Id': Knack.application_id,
    'X-Knack-REST-API-Key': 'YOUR_API_KEY_HERE',
    'Content-Type': 'application/json'
  }
});
const projectData = await getProject.json();

const projectTypeArray = projectData.field_171_raw || [];
if (!Array.isArray(projectTypeArray) || projectTypeArray.length === 0) {
  alert('⚠️ No Project Type found for this project.');
  return;
}

const projectTypeId = projectTypeArray[0].id;
const projectTypeName = projectTypeArray[0].identifier;

console.log('🏷️ Project Type:', projectTypeName, '| ID:', projectTypeId);


:brain: Understanding the _raw Field

Notice the code uses field_171_raw instead of field_171 — this is intentional.
Connecting fields in Knack return an array containing both the ID and the display value.

Example:

field_171: "<span class='6931ccd07576d6391fde7531' data-kn='connection-value'>Weather Storm Inspection</span>"
field_171_raw: [
  { "id": "6931ccd07576d6391fde7531", "identifier": "Lite Renovation" }
]

Always use the _raw version to access structured data programmatically.


:repeat_button: The Automation Logic

Once you’ve retrieved your Project Type, the next step is simple:

Whenever a Project is assigned to a Project Type, clone all associated Task Templates into the Task object for that Project.

In this setup:

  • The trigger occurs in View 42.

  • The script queries the Project Type.

  • It then duplicates every Task Template that belongs to that type into the Task table.

You’ll end up with a fully generated task list every time a new project is created —
without burning through Knack flows or Zapier executions.


Here is my code for the workflow:

// :white_check_mark: View 42: Auto-clone template tasks into Actual Tasks when project is viewed/created

$(document).on(‘knack-view-render.view_42’, async function () {

try {

console.log('🟢 View 42 loaded → Starting Template→Actual Task cloning flow...');



// Step 1️⃣ — Get Project ID from view or URL

const view = Knack.views\['view_42'\];

const projectId =

  view?.model?.attributes?.id ||

  view?.model?.data?.id ||

  (window.location.hash.match(/\\/(\[a-f0-9\]{24})(?:\\/|$)/i) || \[\])\[1\];



if (!projectId) {

  alert('❌ Could not find Project ID.');

  return;

}

console.log('📋 Project ID:', projectId);



// Step 2️⃣ — Get Project Type ID from the Project record

const getProjectUrl = \`https://api.knack.com/v1/objects/object_26/records/${projectId}\`;

const getProject = await fetch(getProjectUrl, {

  headers: {

    'X-Knack-Application-Id': Knack.application_id,

    'X-Knack-REST-API-Key': ’Insert your api here’,

    'Content-Type': 'application/json'

  }

});

const projectData = await getProject.json();

const projectTypeArray = projectData.field_171_raw || \[\];

if (!Array.isArray(projectTypeArray) || projectTypeArray.length === 0) {

  alert('⚠️ No Project Type found for this project.');

  return;

}

const projectTypeId = projectTypeArray\[0\].id;

const projectTypeName = projectTypeArray\[0\].identifier;

console.log('🏷️ Project Type:', projectTypeName, '| ID:', projectTypeId);



// Step 3️⃣ — Get Template Tasks matching this Project Type

const filters = \[

  { field: 'field_178', operator: 'is', value: projectTypeId }

\];

const templateUrl = \`https://api.knack.com/v1/objects/object_28/records?rows_per_page=1000&filters=${encodeURIComponent(JSON.stringify(filters))}\`;



const getTemplates = await fetch(templateUrl, {

  headers: {

    'X-Knack-Application-Id': Knack.application_id,

    'X-Knack-REST-API-Key': ‘Insert your API here’

  }

});



const templateData = await getTemplates.json();

const templates = templateData.records || \[\];



if (templates.length === 0) {

  alert('⚠️ No template tasks found for Project Type: ' + projectTypeName);

  console.warn('No templates found for:', projectTypeId);

  return;

}



console.log(\`📋 Found ${templates.length} Template Tasks for ${projectTypeName}\`);



// Step 4️⃣ — Clone Template Tasks into Actual Tasks

for (const t of templates) {

  const payload = {

    // Map template fields → actual fields

    field_168: t.field_167, // Task Name

    field_179: t.field_173, // Task Description

    field_180: t.field_174, // Task Status

    field_181: t.field_175, // Due Date

    field_183: t.field_177, // Estimate Hours

    field_185: \[t.id\],      // Connection → Template Task

    field_184: \[projectId\]  // Connection → Project

  };



  const postUrl = 'https://api.knack.com/v1/objects/object_29/records';

  console.log('📦 Creating Actual Task:', payload);



  const res = await fetch(postUrl, {

    method: 'POST',

    headers: {

      'X-Knack-Application-Id': Knack.application_id,

      'X-Knack-REST-API-Key': 'Inset your API in here’,

      'Content-Type': 'application/json'

    },

    body: JSON.stringify(payload)

  });



  const txt = await res.text();

  console.log(\`📩 Task "${t.field_167}" → ${res.status}:\`, txt);



  if (!res.ok) console.warn(\`⚠️ Failed to create task "${t.field_167}"\`);

}



// Step 5️⃣ — Final confirmation

alert(\`🎉 Successfully cloned ${templates.length} Template Tasks into Actual Tasks for "${projectTypeName}"!\`);

} catch (err) {

console.error('💥 Error in cloning flow:', err);

alert('❌ Error in cloning flow. Check console for details.');

}

});

3 Likes

Nice write-up @Paul6! I do something similar in construction management apps I’ve built.

The one thing I’d recommend is to never expose the X-Knack-REST-API-Key in the JavaScript code, and to use view-based requests to GET, PUT, and POST records with a user token authorization instead.

1 Like

For sure.

This is beta just to test.

This is so correct and useful, and most economic.

I applied the same principles to Quote templates for my CRM.

Since my app is embeded inside a WordPress website, I linked the JS code to the WP Code plugin, and from there it calls the ID keys that are highly protected inside my external web host.

Sending quotes this way is EXTREMELY FAST.

Very useful. Knack should innovate with a Template functionality.

thanks for sharing @Paul6 !

1 Like

I think it will be a game changer if knack able to create templates.

So, after attempting multiple time. I am unable to use the view base method to perform this task. Since I am using a parents UID it does not reconginzed.

Hi @Paul6,

Here’s a video demo of the simplest API method I use: Creating Task Templates

Expand for code
$(document).on(`knack-form-submit.any`, async function (event, view, record) {
  
  if (
    view.key === 'view_3' || // Add project form
    view.key === 'view_13' // Add tasks from template form 
  ) {
    await addTemplateRecords(record, { 
      taskProjectKey: 'field_22', // Tasks -> Project
      copyFromKey: 'field_30', // Tasks - Task to copy from
      taskTemplateKey: 'field_24', // Tasks -> Template
      projectTemplatesKey: 'field_25', // Projects -> Template(s) 
      tasksGridKey: 'view_9', // Tasks grid
      taskFormKey: 'view_10' // Add task form
    });
    
  }

});

async function addTemplateRecords(record, { taskProjectKey, copyFromKey, taskTemplateKey, projectTemplatesKey, tasksGridKey, taskFormKey }) {

  const projectId = record.id;
  const templateIds = record?.[`${projectTemplatesKey}_raw`].map(r => r.id) || [];
  
  // For each template specified, get all tasks belonging to that template...
  for (const templateId of templateIds) {
    const templateTasksFilters = [{field: taskTemplateKey, operator: 'is', value: templateId}];
    const tasks = await KnackAPI.makeRequest('getMany', { scene: getSceneFromView(tasksGridKey)?.key, view: tasksGridKey, filters: templateTasksFilters }); // Get tasks connected to template

    // For each task found in the template, add a record referencing the project 
    const recordsToCreate = [];
    for (const task of tasks) {
      recordsToCreate.push({
        [copyFromKey]: task.id,
        [taskProjectKey]: projectId
      })
    }

    await KnackAPI.makeRequest('postMany', { scene: getSceneFromView(taskFormKey)?.key , view: taskFormKey, records: recordsToCreate }); // Add task form
    
  }

  // Get scene from view key
  function getSceneFromView(viewKey) {
    return Knack.scenes.models.find(scene => scene.views.models.some(view => view.id === viewKey))?.attributes;
  }

}

Note that this utilises Knack API Helper (thanks to Callum Boase), which you can learn how to install in this resource.

Expand for full code with loadExternalFiles() function
loadExternalFiles([
  { type: 'script', module: false, url: 'https://cdn.jsdelivr.net/npm/knack-api-helper@latest/browser.js' }, // Knack API Helper
]);


$(document).on(`knack-form-submit.any`, async function (event, view, record) {
  
  if (
    view.key === 'view_3' || // Add project form
    view.key === 'view_13' // Add tasks from template form 
  ) {
    await addTemplateRecords(record, { 
      taskProjectKey: 'field_22', // Tasks -> Project
      copyFromKey: 'field_30', // Tasks - Task to copy from
      taskTemplateKey: 'field_24', // Tasks -> Template
      projectTemplatesKey: 'field_25', // Projects -> Template(s) 
      tasksGridKey: 'view_9', // Tasks grid
      taskFormKey: 'view_10' // Add task form
    });
    
  }

});

async function addTemplateRecords(record, { taskProjectKey, copyFromKey, taskTemplateKey, projectTemplatesKey, tasksGridKey, taskFormKey }) {

  const projectId = record.id;
  const templateIds = record?.[`${projectTemplatesKey}_raw`].map(r => r.id) || [];
  
  // For each template specified, get all tasks belonging to that template...
  for (const templateId of templateIds) {
    const templateTasksFilters = [{field: taskTemplateKey, operator: 'is', value: templateId}];
    const tasks = await KnackAPI.makeRequest('getMany', { scene: getSceneFromView(tasksGridKey)?.key, view: tasksGridKey, filters: templateTasksFilters }); // Get tasks connected to template

    // For each task found in the template, add a record referencing the project 
    const recordsToCreate = [];
    for (const task of tasks) {
      recordsToCreate.push({
        [copyFromKey]: task.id,
        [taskProjectKey]: projectId
      })
    }

    await KnackAPI.makeRequest('postMany', { scene: getSceneFromView(taskFormKey)?.key , view: taskFormKey, records: recordsToCreate }); // Add task form
    
  }

  // Get scene from view key
  function getSceneFromView(viewKey) {
    return Knack.scenes.models.find(scene => scene.views.models.some(view => view.id === viewKey))?.attributes;
  }

}

// --------------------------------------------------------------------------------------------------------
//Generic helper function to loader external JS and CSS files before your Knack app finishes loading
function loadExternalFiles(files) {
  KnackInitAsync = function ($, callback) {
    Knack.showSpinner();
    window.$ = window.jQuery = $;

    let loaded = 0;

    files.forEach(file => {
      const el = document.createElement(file.type === 'script' ? 'script' : 'link');

      if (file.type === 'script') {
        el.src = file.url;
        el.async = false;
        if (file.module) el.type = 'module';
      } else {
        el.href = file.url;
        el.rel = 'stylesheet';
      }

      el.onload = () => {
        console.log(`loaded file ${file.url}`);
        if (++loaded === files.length) {
          console.log('all external files loaded');
          callback();
        }
      };

      el.onerror = () => {
        alert(`Error loading ${file.url}. Please refresh and try again.`);
        Knack.hideSpinner();
      };

      document.head.appendChild(el);
    });
  };
}

Let me know your feedback!

4 Likes

Terrific stuff @StephenChapman
I need an excuse to work with @Callum.Boase Knack API Helper!

2 Likes