🧪 killBottleneck je ve veřejné betě — cloud i self-host.🧪 killBottleneck je v betě.Beta na GitHubu →
Skip to content

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.

První postup po importu: spouštěč z tabulky, uzel s kódem a zápis id zpátky do řádku.

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:

  1. 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).
  2. Pravidlo — u nástěnky ho přinese šablona sama, u stromečku si přidáte jedno.
  3. 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

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:

javascript
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:

javascript
const KB_VETEV  = 'Nové reklamace'; // kanbanová nástěnka: 'D1 – Sestavení týmu'
const KB_KANBAN = false;            // kanbanová nástěnka? dejte true

Hodnoty se řídí tvarem mapy z kroku 1:

Tvar mapyKB_VETEVKB_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ě"
javascript
// ⚙️ 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"
javascript
// 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íteCo 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ápisuKlíč má oprávnění jen na čtení. Vytvořte nový s Čtení i zápis.
409 pořád dokolaNěkdo mapu zrovna hodně upravuje. Postup to sám opakuje; když to trvá, zkuste to za chvíli.
429Moc 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 nerozbaliloPravidlo neexistuje, je vypnuté, nebo jeho podmínka míří jinam. Zkontrolujte v Pravidla → log běhů.
ECONNREFUSED / časový limitn8n 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

fair-code — self-hosting a interní použití zdarma, přeprodej jako hostovaná služba ne.