🧪 killBottleneck is in public beta — cloud and self-host.🧪 killBottleneck is in beta.Beta on GitHub →
Skip to content

Connecting through n8n

You run n8n and want it to create work in killBottleneck for you — from a spreadsheet, a form, an e-mail, anywhere? This page walks you through it. Nothing is written here: you download, import, and fill in four lines.

The example runs on complaints handled with the 8D method, but it holds for anything else — orders, hiring, service calls.

The first workflow after importing: the trigger from the sheet, the code node and the id written back into the row.

Why n8n

The call goes from n8n to killBottleneck, so an internal address (http://192.168.1.10:8090) is enough and you expose nothing to the internet. Google Apps Script cannot do that — it runs at Google, so it never reaches a server inside your network.

Before you start

You need a map and an API key. Both are done in killBottleneck and described in the first three steps of Connecting Google Sheets:

  1. The map — a tree (every complaint carries its own eight disciplines) or a kanban board (eight columns with the complaint as a card travelling across them).
  2. The rule — the board template brings it along, on a tree you add one.
  3. An API key with the Read and write permission.

Then come back here.

1. Download the workflows

In n8n import both via Workflows → ⋯ → Import from File.

2. Fill in the configuration

In the first workflow open the Create the complaint in the map node. There are four lines at the top:

javascript
const KB_ADDRESS = 'https://yourcompany.killbottleneck.com'; // no trailing slash
const KB_KEY     = 'kb_user_paste_your_key_here';            // API key with write permission
const KB_MAP     = 'paste_your_map_id_here';                 // from the address /map/xxxxx
const KB_BRANCH  = 'New complaints';                         // what complaints hang under

In the second workflow, in the Compute the progress node, there are two:

javascript
const KB_BRANCH = 'New complaints'; // kanban board: 'D1 – Team formation'
const KB_KANBAN = false;            // a kanban board? set true

The values follow the shape of the map from step 1:

Shape of the mapKB_BRANCHKB_KANBAN
Tree'New complaints'false
Board'D1 – Team formation' (the first column)true

The key does not have to live in the code

Put it in an n8n environment variable and replace the line with const KB_KEY = $env.KB_KEY;.

3. Add Google and switch it on

On the green (Google Sheets) nodes pick your Google credentials and set the spreadsheet and sheet. Then switch both workflows on — the first waits for a new row, the second runs on a timer (every 10 minutes by default).

The sheet then gets sentences like D2 – Problem description (1/8); on a board that is the column the card currently stands in.

What the code inside does

You do not have to read it, but so you know why it is not a single call: on every write killBottleneck wants to know which version of the map you saw (base_updated). If somebody changed it meanwhile, it answers 409 and asks you to read the map again — a safeguard against overwriting other people's work. So the workflow reads the map, writes, and on a conflict tries again (up to four times).

The whole code of the "Create the complaint in the map" node
javascript
// ⚙️ FILL IN FOUR LINES ------------------------------------------
const KB_ADDRESS = 'https://yourcompany.killbottleneck.com'; // no trailing slash
const KB_KEY     = 'kb_user_paste_your_key_here';            // API key with write permission
const KB_MAP     = 'paste_your_map_id_here';                 // from the address /map/xxxxx
const KB_BRANCH  = 'New complaints';                         // what complaints hang under
// ---------------------------------------------------------------

// Tip: the key does not have to live in the code. Put it in an n8n environment
// variable and replace the line above with:  const KB_KEY = $env.KB_KEY;

const headers = { Authorization: 'Bearer ' + KB_KEY };

/** Walks the map tree and finds a node by its title. */
function findByTitle(nodes, title) {
  for (const u of nodes || []) {
    if (u.title === title) return u;
    const deeper = findByTitle(u.children, title);
    if (deeper) return deeper;
  }
  return null;
}

/** Digs the HTTP code out of the error, wherever n8n hides it. */
function errorCode(e) {
  return Number(e.httpCode || e.statusCode || (e.response && (e.response.status || e.response.statusCode)) || 0);
}

const results = [];

for (const item of $input.all()) {
  const row = item.json;
  const title = 'Complaint ' + (row['Complaint number'] || '?') + ' — ' + (row['Customer'] || '');
  const description = String(row['Problem description'] || '');

  let goalId = null;
  let lastError = '';

  // Up to four attempts. On every write killBottleneck wants to know which version
  // of the map we saw (base_updated). If somebody changed it meanwhile it answers 409
  // and asks us to read the map again — a safeguard against overwriting other work.
  for (let attempt = 1; attempt <= 4 && !goalId; attempt++) {
    const map = await this.helpers.httpRequest({
      method: 'GET',
      url: KB_ADDRESS + '/api/kb/v1/maps/' + KB_MAP,
      headers: headers,
      json: true,
    });

    const branch = findByTitle(map.tree, KB_BRANCH);
    if (!branch) throw new Error('The map has no "' + KB_BRANCH + '" branch.');

    try {
      const response = await this.helpers.httpRequest({
        method: 'POST',
        url: KB_ADDRESS + '/api/kb/v1/maps/' + KB_MAP + '/nodes',
        headers: headers,
        body: {
          base_updated: map.updated,
          parent_id: branch.id,
          items: [{ title: title, description: description }],
        },
        json: true,
      });
      goalId = response.added_ids[0];
    } catch (e) {
      const code = errorCode(e);
      lastError = code + ' ' + e.message;
      if (code === 401) throw new Error('Invalid API key — check KB_KEY.');
      if (code === 404) throw new Error('Map not found — check KB_MAP.');
      if (code !== 409 && code !== 429) throw e;
      await new Promise((r) => setTimeout(r, 700 * attempt)); // somebody changed the map, retry
    }
  }

  if (!goalId) throw new Error('The goal could not be created: ' + lastError);

  results.push({ json: Object.assign({}, row, { killBottleneck: goalId, kb_title: title }) });
}

return results;
The whole code of the "Compute the progress" node
javascript
// Turns the map into a list of "card → how far the 8D got", which then flows into the sheet.
// ⚙️ FILL IN TWO LINES — the same as in the first workflow:
const KB_BRANCH = 'New complaints'; // kanban board: 'D1 – Team formation'
const KB_KANBAN = false;            // a kanban board? set true

/** An index of goals: the parent and the row the parent stands in (the columns, on a kanban). */
function index(tree) {
  const all = {};
  const walk = (goals, parent, parentRow) => {
    (goals || []).forEach((u) => {
      all[u.id] = { goal: u, parent, parentRow };
      walk(u.children, u, goals);
    });
  };
  walk(tree, null, []);
  return all;
}

/** One sentence for the sheet — the column (kanban), or the done subgoals (classic map). */
function progressText(entry) {
  if (KB_KANBAN) {
    const columns = entry.parentRow || [];
    const position = columns.indexOf(entry.parent) + 1;
    if (!position) return 'not under any column';
    if (position === columns.length && entry.goal.status === 'done') return '✅ closed (' + position + '/' + columns.length + ')';
    return entry.parent.title + ' (' + position + '/' + columns.length + ')';
  }
  const steps = entry.goal.children || [];
  if (!steps.length) return entry.goal.status === 'done' ? 'closed' : 'waiting for the 8D to expand';
  const done = steps.filter((k) => k.status === 'done').length;
  if (done === steps.length) return '✅ closed (' + done + '/' + steps.length + ')';
  return steps.filter((k) => k.status !== 'done')[0].title + ' (' + done + '/' + steps.length + ')';
}

const map = $input.first().json;
const all = index(map.tree);

const entry = Object.values(all).filter((z) => z.goal.title === KB_BRANCH)[0];
if (!entry) throw new Error('The map has no "' + KB_BRANCH + '" goal.');

// Classic map: cards hang under one goal. Kanban: they are spread across all columns of
// the row the first one stands in — hence the siblings of the entry node.
const columns = KB_KANBAN ? (entry.parent ? entry.parent.children || [] : [entry.goal]) : [entry.goal];
const cards = columns.reduce((acc, s) => acc.concat(s.children || []), []);

return cards.map((card) => ({
  json: {
    killBottleneck: card.id,
    '8D status': progressText(all[card.id]),
  },
}));

When it does not work

What you seeWhat is going on
Invalid API key (401)The key is mis-copied, revoked or expired. Create a new one.
Map not found (404)Wrong map id — or the key belongs to someone who cannot reach that map.
403 on a writeThe key is read-only. Create a new one with Read and write.
409 over and overSomebody is editing the map heavily right now. The workflow retries on its own; if it persists, try again later.
429Too many writes per minute (the cap is 30). Send rows in batches, not one by one.
The map has no … goalKB_BRANCH does not match a title in the map. On a board it must be the first column.
The goal appears but the 8D does not expandThe rule is missing, disabled, or its condition points elsewhere. Check Rules → run log.
ECONNREFUSED / a timeoutn8n cannot reach the killBottleneck address. Check KB_ADDRESS — from the n8n container localhost is not your computer.

What to expect, and what not

  • Deadlines are in days, not hours. "Interim containment within 24 hours" can be set no finer than tomorrow.
  • The order of goals in the map is not the order they were created in — which is why the workflows remember the goal id.
  • Rules only ever apply going forward; what is already in the map is not expanded.
  • killBottleneck does not produce the finished 8D form for your customer. It watches the progress and the deadlines.

Where next

fair-code — self-hosting and internal use are free, reselling as a hosted service is not.