Napojení přes n8n
Máte n8n a chcete, aby vám do killBottlenecku samo zakládalo práci — z tabulky, z formuláře, z e-mailu, odkudkoli? Tahle stránka je návod krok za krokem. Nic se tu nepíše, jen se stahuje, importuje a vyplňují se čtyři řádky.
Příklad tu jede na reklamacích řešených metodou 8D, ale platí to pro cokoli jiného — objednávky, nábor, servisní zásahy.
Proč zrovna n8n
Volání jde z n8n do killBottlenecku, takže si vystačíte s vnitřní adresou (http://192.168.1.10:8090) a nemusíte nic vystavovat do internetu. Skripty Google Apps Script tohle neumí — běží u Googlu, takže na server ve firemní síti nedosáhnou.
Než začnete
Potřebujete mapu a API klíč. Obojí se dělá v killBottlenecku a je popsané v prvních třech krocích návodu Napojení na Google Sheets:
- Mapa — stromeček (každá reklamace má osm disciplín pod sebou), nebo kanbanová nástěnka (osm sloupců a reklamace je karta, která jimi putuje).
- Pravidlo — u nástěnky ho přinese šablona sama, u stromečku si přidáte jedno.
- API klíč s oprávněním Čtení i zápis.
Pak se vraťte sem.
Nemusí to být jen reklamace
Stejným postupem se zakládá jakákoli práce — a když se má vracet v rytmu (páteční uzávěrka, měsíční závěrka, pravidelný audit), udělá to cíl sám: viz Práce, která se vrací sama.
1. Stáhněte si hotové postupy
- Nová reklamace → killBottleneck — z nového řádku v tabulce založí reklamaci v mapě a id si zapíše zpátky do řádku
- Stav 8D → zpátky do tabulky — jednou za čas projde mapu a doplní, kde řešení stojí
V n8n je naimportujete přes Workflows → ⋯ → Import from File.
2. Vyplňte nastavení
V prvním postupu otevřete uzel Založ reklamaci v mapě. Nahoře jsou čtyři řádky:
const KB_ADRESA = 'https://vasefirma.killbottleneck.com'; // bez lomítka na konci
const KB_KLIC = 'kb_user_sem_vlozte_svuj_klic'; // API klíč s právem zápisu
const KB_MAPA = 'sem_vlozte_id_mapy'; // z adresy /mapa/xxxxx
const KB_VETEV = 'Nové reklamace'; // pod co se reklamace věšíVe druhém postupu, v uzlu Spočítej pokrok, jsou řádky dva:
const KB_VETEV = 'Nové reklamace'; // kanbanová nástěnka: 'D1 – Sestavení týmu'
const KB_KANBAN = false; // kanbanová nástěnka? dejte trueHodnoty se řídí tvarem mapy z kroku 1:
| Tvar mapy | KB_VETEV | KB_KANBAN |
|---|---|---|
| Stromeček | 'Nové reklamace' | false |
| Nástěnka | 'D1 – Sestavení týmu' (první sloupec) | true |
Klíč nemusí ležet v kódu
Když si ho vložíte do proměnné prostředí n8n, nahraďte řádek za const KB_KLIC = $env.KB_KLIC;.
3. Doplňte Google a zapněte
U zelených uzlů (Google Sheets) vyberte své přihlášení ke Googlu a nastavte tabulku a list. Pak oba postupy zapněte — první čeká na nový řádek, druhý jede podle časovače (výchozí každých 10 minut).
Do tabulky pak chodí věty jako D2 – Popis problému (1/8); u nástěnky je to sloupec, ve kterém karta právě stojí.
Co dělá ten kód uvnitř
Nemusíte to číst, ale ať víte, proč to není jedno jediné volání: killBottleneck u každého zápisu chce vědět, jakou verzi mapy jste viděli (base_updated). Když ji někdo mezitím změnil, odpoví 409 a chce, ať si ji načtete znovu — je to pojistka proti přepsání cizí práce. Postup si proto mapu načte, zapíše, a při konfliktu to zkusí znovu (až čtyřikrát).
Celý kód uzlu „Založ reklamaci v mapě"
// ⚙️ VYPLŇTE ČTYŘI ŘÁDKY -----------------------------------------
const KB_ADRESA = 'https://vasefirma.killbottleneck.com'; // bez lomítka na konci
const KB_KLIC = 'kb_user_sem_vlozte_svuj_klic'; // API klíč s právem zápisu
const KB_MAPA = 'sem_vlozte_id_mapy'; // z adresy /mapa/xxxxx
const KB_VETEV = 'Nové reklamace'; // pod co se reklamace věší
// ---------------------------------------------------------------
// Tip pro pokročilé: klíč nemusí být v kódu. Když si ho vložíte do proměnné
// prostředí n8n, nahraďte řádek výše za: const KB_KLIC = $env.KB_KLIC;
const hlavicky = { Authorization: 'Bearer ' + KB_KLIC };
/** Projde strom mapy a najde uzel podle názvu. */
function najdiPodleNazvu(uzly, nazev) {
for (const u of uzly || []) {
if (u.title === nazev) return u;
const hlubsi = najdiPodleNazvu(u.children, nazev);
if (hlubsi) return hlubsi;
}
return null;
}
/** Z chyby vytáhne HTTP kód, ať už ho n8n schová kamkoli. */
function kodChyby(e) {
return Number(e.httpCode || e.statusCode || (e.response && (e.response.status || e.response.statusCode)) || 0);
}
const vysledky = [];
for (const polozka of $input.all()) {
const radek = polozka.json;
const nazev = 'Reklamace ' + (radek['Číslo reklamace'] || '?') + ' — ' + (radek['Zákazník'] || '');
const popis = String(radek['Popis problému'] || '');
let uzelId = null;
let posledniChyba = '';
// Až čtyři pokusy. killBottleneck u každého zápisu chce vědět, jakou verzi mapy
// jsme viděli (base_updated). Když ji někdo mezitím změnil, odpoví 409 a chce,
// ať si mapu načteme znovu — je to pojistka proti přepsání cizí práce.
for (let pokus = 1; pokus <= 4 && !uzelId; pokus++) {
const mapa = await this.helpers.httpRequest({
method: 'GET',
url: KB_ADRESA + '/api/kb/v1/maps/' + KB_MAPA,
headers: hlavicky,
json: true,
});
const vetev = najdiPodleNazvu(mapa.tree, KB_VETEV);
if (!vetev) throw new Error('V mapě chybí větev „' + KB_VETEV + '".');
try {
const odpoved = await this.helpers.httpRequest({
method: 'POST',
url: KB_ADRESA + '/api/kb/v1/maps/' + KB_MAPA + '/nodes',
headers: hlavicky,
body: {
base_updated: mapa.updated,
parent_id: vetev.id,
items: [{ title: nazev, description: popis }],
},
json: true,
});
uzelId = odpoved.added_ids[0];
} catch (e) {
const kod = kodChyby(e);
posledniChyba = kod + ' ' + e.message;
if (kod === 401) throw new Error('Neplatný API klíč — zkontrolujte KB_KLIC.');
if (kod === 404) throw new Error('Mapa nenalezena — zkontrolujte KB_MAPA.');
if (kod !== 409 && kod !== 429) throw e;
await new Promise((r) => setTimeout(r, 700 * pokus)); // někdo mapu změnil, zkusíme znovu
}
}
if (!uzelId) throw new Error('Uzel se nepodařilo založit: ' + posledniChyba);
vysledky.push({ json: Object.assign({}, radek, { killBottleneck: uzelId, kb_nazev: nazev }) });
}
return vysledky;Celý kód uzlu „Spočítej pokrok"
// Z mapy udělá seznam „karta → jak daleko je 8D", který se pak nalije do tabulky.
// ⚙️ VYPLŇTE DVA ŘÁDKY — stejné jako v prvním postupu:
const KB_VETEV = 'Nové reklamace'; // kanbanová nástěnka: 'D1 – Sestavení týmu'
const KB_KANBAN = false; // kanbanová nástěnka? dejte true
/** Rejstřík uzlů: u každého rodič a řada, ve které stojí rodič (u kanbanu = sloupce). */
function rejstrik(strom) {
const vse = {};
const projdi = (uzly, rodic, radaRodice) => {
(uzly || []).forEach((u) => {
vse[u.id] = { uzel: u, rodic, radaRodice };
projdi(u.children, u, uzly);
});
};
projdi(strom, null, []);
return vse;
}
/** Jedna věta do tabulky — sloupec (kanban), nebo hotové podcíle (klasická mapa). */
function popisPokroku(zaznam) {
if (KB_KANBAN) {
const sloupce = zaznam.radaRodice || [];
const poradi = sloupce.indexOf(zaznam.rodic) + 1;
if (!poradi) return 'není pod žádným sloupcem';
if (poradi === sloupce.length && zaznam.uzel.status === 'done') return '✅ uzavřeno (' + poradi + '/' + sloupce.length + ')';
return zaznam.rodic.title + ' (' + poradi + '/' + sloupce.length + ')';
}
const kroky = zaznam.uzel.children || [];
if (!kroky.length) return zaznam.uzel.status === 'done' ? 'uzavřeno' : 'čeká na rozbalení 8D';
const hotovo = kroky.filter((k) => k.status === 'done').length;
if (hotovo === kroky.length) return '✅ uzavřeno (' + hotovo + '/' + kroky.length + ')';
return kroky.filter((k) => k.status !== 'done')[0].title + ' (' + hotovo + '/' + kroky.length + ')';
}
const mapa = $input.first().json;
const vse = rejstrik(mapa.tree);
const vstup = Object.values(vse).filter((z) => z.uzel.title === KB_VETEV)[0];
if (!vstup) throw new Error('V mapě chybí cíl „' + KB_VETEV + '".');
// Klasická mapa: karty visí pod jedním cílem. Kanban: jsou rozeseté po všech sloupcích
// řady, ve které stojí i ten první — proto se berou sourozenci vstupního uzlu.
const sloupce = KB_KANBAN ? (vstup.rodic ? vstup.rodic.children || [] : [vstup.uzel]) : [vstup.uzel];
const karty = sloupce.reduce((acc, s) => acc.concat(s.children || []), []);
return karty.map((karta) => ({
json: {
killBottleneck: karta.id,
'Stav 8D': popisPokroku(vse[karta.id]),
},
}));Když to nefunguje
| Co vidíte | Co se děje |
|---|---|
Neplatný API klíč (401) | Klíč je špatně zkopírovaný, zrušený, nebo prošla platnost. Vytvořte nový. |
Mapa nenalezena (404) | Špatné id mapy — nebo klíč patří někomu, kdo na tu mapu nedosáhne. |
403 u zápisu | Klíč má oprávnění jen na čtení. Vytvořte nový s Čtení i zápis. |
409 pořád dokola | Někdo mapu zrovna hodně upravuje. Postup to sám opakuje; když to trvá, zkuste to za chvíli. |
429 | Moc zápisů za minutu (strop je 30). Posílejte řádky po dávkách, ne po jednom. |
V mapě chybí cíl … | KB_VETEV nesedí na název v mapě. U nástěnky to musí být první sloupec. |
| Cíl vznikl, ale 8D se nerozbalilo | Pravidlo neexistuje, je vypnuté, nebo jeho podmínka míří jinam. Zkontrolujte v Pravidla → log běhů. |
ECONNREFUSED / časový limit | n8n na adresu killBottlenecku nedosáhne. Zkontrolujte KB_ADRESA — z kontejneru n8n není localhost váš počítač. |
Co čekat a co ne
- Termíny jsou po dnech, ne po hodinách. „Okamžité opatření do 24 hodin" umí pravidlo nastavit nejjemněji na zítra.
- Pořadí cílů v mapě není pořadí zakládání — postupy si proto pamatují id cíle.
- Pravidla platí jen do budoucna; co v mapě už je, se zpětně nerozbalí.
- killBottleneck nevyrobí hotový 8D formulář pro zákazníka. Hlídá průběh a termíny.
Kam dál
- Napojení na Google Sheets — totéž bez n8n, přes Apps Script
- REST API — všechny endpointy, limity a chybové kódy
- AI agenti — opačný směr: mapa volá vaše n8n workflow

