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.

It does not have to be complaints

The same workflow creates any kind of work — and when it should come back in a rhythm (a Friday closing, a monthly close, a regular audit), the goal handles that itself: see Work that comes back on its own.

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.