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.
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:
- 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).
- The rule — the board template brings it along, on a tree you add one.
- An API key with the Read and write permission.
Then come back here.
1. Download the workflows
- New complaint → killBottleneck — turns a new row into a complaint in the map and writes the id back into the row
- 8D status → back to the sheet — walks the map every so often and fills in where the work stands
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:
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 underIn the second workflow, in the Compute the progress node, there are two:
const KB_BRANCH = 'New complaints'; // kanban board: 'D1 – Team formation'
const KB_KANBAN = false; // a kanban board? set trueThe values follow the shape of the map from step 1:
| Shape of the map | KB_BRANCH | KB_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
// ⚙️ 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
// 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 see | What 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 write | The key is read-only. Create a new one with Read and write. |
409 over and over | Somebody is editing the map heavily right now. The workflow retries on its own; if it persists, try again later. |
429 | Too many writes per minute (the cap is 30). Send rows in batches, not one by one. |
The map has no … goal | KB_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 expand | The rule is missing, disabled, or its condition points elsewhere. Check Rules → run log. |
ECONNREFUSED / a timeout | n8n 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
- Connecting Google Sheets — the same thing without n8n, via Apps Script
- REST API — every endpoint, the limits and the error codes
- AI agents — the other direction: the map calls your n8n workflow

