Napojení na Google Sheets
Máte evidenci v Google Sheets — třeba reklamace — a chcete, aby se z každého nového řádku v killBottlenecku sám rozjel celý postup řešení? Tahle stránka je návod krok za krokem. Programovat umět nemusíte: kód je hotový, vyplníte v něm pár řádků.
Jako příklad tu jede reklamace řešená metodou 8D, ale stejně se to dá použít na cokoli jiného — objednávky, nábor, servisní zásahy. Mění se jen názvy.
Co přesně se stane
- Do tabulky přibude řádek s novou reklamací.
- Do mapy se založí cíl „Reklamace 0001 — Auto Novák s.r.o.".
- killBottleneck mu sám rozbalí osm disciplín D1–D8 a dá vědět, komu to spadlo.
- Jak lidé disciplíny odškrtávají, píše se zpátky do tabulky „D4 – Kořenová příčina (3/8)".
Používáte n8n? Jděte rovnou jinam
Tahle stránka vede tabulku přes Google Apps Script, který je součástí Sheets a nic dalšího nepotřebuje. Máte-li n8n, je to pro vás jednodušší i mocnější cesta — kroky 1–3 níže jsou společné a pak pokračujte v Napojení přes n8n.
Než začnete
Potřebujete tři věci a zhruba půl hodiny:
- účet v killBottlenecku — funguje to úplně stejně v cloudu i na vlastním serveru,
- tabulku v Google Sheets, kde má první řádek názvy sloupců,
- deset minut na klikání a deset na vložení hotového kódu.
Vlastní server ve firemní síti? Přečtěte si tohle první
Skripty Google Apps Script neběží u vás v kanceláři, ale na serverech Googlu. Aby Google mohl do killBottlenecku zapsat, musí na něj z internetu dosáhnout.
- Cloudová instance (
vasefirma.killbottleneck.com) — dosáhne, nic neřešíte. - Vlastní server na veřejné adrese — dosáhne, jen ať má HTTPS.
- Vlastní server jen ve firemní síti — nedosáhne. Buď ho vystavíte na veřejnou adresu, nebo jděte cestou přes n8n a rozběhněte ho ve stejné síti. Opačný směr — killBottleneck píšící ven do tabulky — funguje odkudkoli.
1. Připravte si mapu: stromeček, nebo nástěnka?
Nejdřív se rozhodněte, jak má práce v mapě vypadat. Obojí umí totéž, liší se pohledem — a od téhle volby se pak liší dva řádky nastavení v receptu, nic víc.
Varianta A — stromeček (výchozí)
Každá reklamace má svých osm disciplín pod sebou.
Založte nový projekt (třeba Reklamace 2026) a pod hlavní cíl dejte jeden cíl, kterému budete říkat třeba Nové reklamace. Právě pod něj se budou nové řádky věšet.
Varianta B — kanbanová nástěnka
Osm sloupců vedle sebe a reklamace je karta, která jimi putuje.
Založíte ji na dvě kliknutí: Nový projekt → Ze šablony → 8D report — kanban.
| Kdy sáhnout po které | |
|---|---|
| Stromeček | Chcete u každé reklamace vidět všech osm kroků najednou, komu patří a kdy mají být hotové. |
| Nástěnka | Chcete na jeden pohled vidět, kolik případů visí na které disciplíně. |
Ať už zvolíte cokoli, id mapy najdete v adrese, když ji máte otevřenou: …/map/y2cmuebk7guaev1. Zapište si ho, budete ho potřebovat.
2. Ať se postup rozjede sám
Nástěnka — nemusíte dělat nic
Šablona 8D report — kanban přináší sloupce i sedm pravidel posunu. Každé říká: karta pod Dx přepnuta na Hotovo → přesuň ji pod Dx+1 → vrať ji na Založeno. Hotovo, pokračujte krokem 3.
U stromečku si jedno pravidlo přidáte:
- V mapě klikněte na blesk → Pravidla (na užší obrazovce je v nabídce ⋮).
- Nové pravidlo a nastavte:
- Když: Uzel založen
- Pokud: nadřazený cíl je Nové reklamace
- Udělej: Založ podcíle a vypište osm disciplín D1 až D8
Tu podmínku „pokud" nevynechávejte
Bez ní se osm kroků rozbalí úplně každému novému cíli v mapě — tedy i těm osmi, které pravidlo právě založilo. Podmínka na nadřazený cíl je to, co tomu brání.
Chcete rovnou i přiřazení a upozornění? Přidejte akce Přiřaď zodpovědnou osobu a Pošli upozornění. Víc v Automatizačních pravidlech.
3. Vytvořte API klíč
Klíč je heslo, kterým se tabulka killBottlenecku představí.
- Vpravo nahoře uživatelské menu → API klíče.
- Popis třeba
Google Sheets, Oprávnění: Čtení i zápis. - Vytvořit a klíč si hned zkopírujte — ukáže se jen jednou. Když ho ztratíte, nedá se zpětně zjistit, jen zrušit a vydat nový.
Klíč platí za vás a jen na vaše mapy. Přes API se nedá dělat správcovská práce, ani když se ztratí — podrobnosti v REST API.
4. Bonus: práce, která se vrací sama
Reklamace přijde, vyřeší se a je hotová. Spousta práce se ale vrací v rytmu — a nemá smysl ji pokaždé zakládat znovu. Od verze 0.35 to umí cíl sám: v Zadání cíle je přepínač Opakování (denně / týdně / měsíčně).
Co se stane, když takový cíl odbavíte na Hotovo: sám se vrátí na Založeno a termín se posune o jeden interval. U cíle svítí odznak 🔁.
Typicky se hodí na tohle:
| Co | Rytmus | Termín nastavte na |
|---|---|---|
| Uzavřít týden a poslat sumář práce | týdně | nejbližší pátek |
| Měsíční závěrka | měsíčně | den v měsíci, kdy ji děláte (např. 15.) |
| Pravidelný audit | měsíčně | konec měsíce (31. se v kratších měsících přisune) |
Rytmus drží i na zpožděnou práci
Tohle je na tom to hlavní. Kdyby to bylo obyčejné „+7 dní", stačilo by jednou odbavit uzávěrku o tři týdny později a pátky by se rozjely na úterky. Opakování proto počítá od původního termínu a prošlé výskyty přeskočí na nejbližší budoucí.
Ověřeno: uzávěrka s propadlým termínem 31. 7. (pátek) odbavená 17. 8. dostala nový termín 21. 8. — zase pátek, ne 7. 8. Stejně tak 31. leden vyjde na 28. února, ne na 3. březen.
Opakování není žádný nový pojem — je to obyčejné automatizační pravidlo, takže ho uvidíte v Pravidlech mapy, přenese se šablonou a dá se založit i z receptu. Když si chcete pravidelný cíl založit rovnou z tabulky nebo z n8n, pošlete po založení cíle ještě tohle na POST /api/kb/v1/maps/{id}/rules:
{
"name": "Opakování: Uzavřít týden",
"node_id": "ID_CÍLE",
"trigger": { "type": "node_status_changed", "status": "done" },
"actions": [
{ "type": "set_status", "status": "todo" },
{ "type": "set_deadline", "advance": "weekly" }
]
}advance bere daily, weekly nebo monthly. Cíl musí mít termín — bez něj není co posouvat.
Tady končí společná část. Pokračujete-li přes n8n, jděte na Napojení přes n8n. Zbytek téhle stránky je cesta přes Apps Script.
5. Sloupce v tabulce
Připravte si tyhle sloupce (na pořadí nezáleží, na názvech ano):
| Číslo reklamace | Zákazník | Popis problému | killBottleneck | Stav 8D |
|---|---|---|---|---|
| 0001 | Auto Novák s.r.o. | Prasklý svar na držáku. | (nechte prázdné) | (nechte prázdné) |
Poslední dva sloupce si vyplňuje skript sám — do killBottleneck si ukládá, který cíl v mapě řádku odpovídá, do Stav 8D píše, jak řešení pokročilo.
6. Vložte skript
V tabulce Rozšíření → Apps Script, smažte, co tam je, a vložte celý kód níže.
Celý kód pro Apps Script (rozbalit a zkopírovat)
/**
* killBottleneck ↔ Google Sheets
* ------------------------------------------------------------------
* Co to umí:
* 1) z každého nového řádku v evidenci reklamací založí v mapě uzel
* a killBottleneck mu sám rozbalí 8D report,
* 2) zpátky do tabulky píše, jak řešení pokročilo („D4 – Kořenová příčina (3/8)").
*
* Nemusíte umět programovat. Stačí vyplnit nastavení hned pod tímhle textem.
*/
// ⚙️ ------------------- VYPLŇTE NASTAVENÍ -------------------
/** Adresa vaší instance. V cloudu je to to, co máte v prohlížeči, BEZ lomítka na konci. */
const KB_ADRESA = 'https://vasefirma.killbottleneck.com';
/** API klíč. Vytvoříte ho v aplikaci: vaše jméno vpravo nahoře → API klíče → Nový klíč,
* a MUSÍ mít oprávnění „čtení i zápis". Ukáže se jen jednou, tak si ho hned zkopírujte. */
const KB_KLIC = 'kb_user_sem_vlozte_svuj_klic';
/** Id mapy. Je vidět v adrese, když máte mapu otevřenou: /mapa/y2cmuebk7guaev1 */
const KB_MAPA = 'sem_vlozte_id_mapy';
/** Pod který cíl se nové reklamace věší.
* Klasická mapa: 'Nové reklamace'. Kanbanová nástěnka: první sloupec, tedy 'D1 – Sestavení týmu'. */
const KB_VETEV = 'Nové reklamace';
/** Máte kanbanovou nástěnku (karta se po „Hotovo" stěhuje po sloupcích)? Dejte true. */
const KB_KANBAN = false;
// ⚙️ ----------------------------------------------------------
/**
* Jak se jmenují sloupce v tabulce. Skript je hledá podle NÁZVU v prvním řádku,
* takže je můžete mít v libovolném pořadí — jen se musí jmenovat takhle.
*/
const SLOUPCE = {
cislo: 'Číslo reklamace',
zakaznik: 'Zákazník',
popis: 'Popis problému',
odkaz: 'killBottleneck', // sem si skript sám uloží uzel — needitujte ho
stav: 'Stav 8D', // sem si skript sám píše, jak to pokročilo
};
/** Nabídka v tabulce (objeví se po znovuotevření souboru). */
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('killBottleneck')
.addItem('Odeslat nové reklamace', 'odeslatNoveReklamace')
.addItem('Načíst stav řešení', 'nacistStavy')
.addToUi();
}
/**
* KROK 1: každý řádek, který ještě nemá vyplněný sloupec „killBottleneck",
* založí v mapě jako nový cíl. Řádky, které tam už jsou, přeskočí — takže
* se nic nezdvojí, ani když funkci pustíte desetkrát po sobě.
*/
function odeslatNoveReklamace() {
const list = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const data = list.getDataRange().getValues();
const sl = kbNajdiSloupce_(data[0]);
let zalozeno = 0;
for (let r = 1; r < data.length; r++) {
const radek = data[r];
if (radek[sl.odkaz]) continue; // už odesláno
if (!radek[sl.cislo] && !radek[sl.popis]) continue; // prázdný řádek
const nazev = 'Reklamace ' + radek[sl.cislo] + ' — ' + radek[sl.zakaznik];
const popis = String(radek[sl.popis] || '');
const uzelId = kbZalozUzel_(nazev, popis);
list.getRange(r + 1, sl.odkaz + 1).setValue(uzelId);
zalozeno++;
}
kbHlaska_(zalozeno ? 'Odesláno nových reklamací: ' + zalozeno : 'Nic nového k odeslání.');
}
/**
* KROK 2: načte mapu a ke každému řádku dopíše, kde řešení stojí.
* Tohle je ta „zpětná vazba do evidence" — když někdo v mapě splní disciplínu,
* do tabulky se to propíše.
*/
function nacistStavy() {
const list = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const data = list.getDataRange().getValues();
const sl = kbNajdiSloupce_(data[0]);
const mapa = kbVolani_('GET', '/api/kb/v1/maps/' + KB_MAPA);
const podleId = kbRejstrik_(mapa.tree);
let aktualizovano = 0;
for (let r = 1; r < data.length; r++) {
const uzelId = data[r][sl.odkaz];
if (!uzelId) continue;
const zaznam = podleId[uzelId];
const text = zaznam ? kbPopisPokroku_(zaznam) : 'v mapě už není';
if (data[r][sl.stav] !== text) {
list.getRange(r + 1, sl.stav + 1).setValue(text);
aktualizovano++;
}
}
kbHlaska_('Aktualizováno řádků: ' + aktualizovano);
}
/**
* Jedna věta do tabulky. Podle KB_KANBAN se čte jinak:
*
* KLASICKÁ mapa — reklamace má osm disciplín pod sebou, počítá se, kolik je hotových.
* KANBANOVÁ — reklamace je karta bez podcílů, která se po „Hotovo" sama stěhuje
* pod další sloupec; hlásí se sloupec, ve kterém právě stojí.
*/
function kbPopisPokroku_(zaznam) {
const uzel = zaznam.uzel;
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 && uzel.status === 'done') return '✅ uzavřeno (' + poradi + '/' + sloupce.length + ')';
return zaznam.rodic.title + ' (' + poradi + '/' + sloupce.length + ')';
}
const kroky = uzel.children || [];
if (!kroky.length) return uzel.status === 'done' ? 'uzavřeno' : 'čeká na rozbalení 8D';
const hotovo = kroky.filter(function (k) { return k.status === 'done'; }).length;
if (hotovo === kroky.length) return '✅ uzavřeno (' + hotovo + '/' + kroky.length + ')';
const prvniNehotovy = kroky.filter(function (k) { return k.status !== 'done'; })[0];
return prvniNehotovy.title + ' (' + hotovo + '/' + kroky.length + ')';
}
/**
* Založí jeden uzel pod cílem z KB_VETEV a vrátí jeho id.
*
* 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ž někdo mapu mezitím změnil,
* odpoví 409 a chce, ať si ji načtete znovu. Je to pojistka proti přepsání cizí
* práce — my ji tady prostě obsloužíme a zkusíme to znovu.
*/
function kbZalozUzel_(nazev, popis) {
for (let pokus = 1; pokus <= 4; pokus++) {
const mapa = kbVolani_('GET', '/api/kb/v1/maps/' + KB_MAPA);
const vetev = kbNajdiPodleNazvu_(mapa.tree, KB_VETEV);
if (!vetev) throw new Error('V mapě chybí cíl „' + KB_VETEV + '".');
const odpoved = kbVolani_('POST', '/api/kb/v1/maps/' + KB_MAPA + '/nodes', {
base_updated: mapa.updated,
parent_id: vetev.id,
items: [{ title: nazev, description: popis }],
}, true);
if (odpoved.kod === 200) return odpoved.telo.added_ids[0];
if (odpoved.kod === 409) { Utilities.sleep(700 * pokus); continue; } // mapu někdo změnil
if (odpoved.kod === 429) { Utilities.sleep(3000 * pokus); continue; } // příliš rychle
throw new Error('killBottleneck odpověděl ' + odpoved.kod + ': ' + JSON.stringify(odpoved.telo));
}
throw new Error('Mapa se pořád mění, zkuste to za chvíli znovu.');
}
/** Jedno volání do killBottlenecku. */
function kbVolani_(metoda, cesta, telo, vratitKod) {
const odpoved = UrlFetchApp.fetch(KB_ADRESA + cesta, {
method: metoda.toLowerCase(),
contentType: 'application/json',
headers: { Authorization: 'Bearer ' + KB_KLIC },
payload: telo ? JSON.stringify(telo) : undefined,
muteHttpExceptions: true,
});
const kod = odpoved.getResponseCode();
let obsah = {};
try { obsah = JSON.parse(odpoved.getContentText()); } catch (e) {}
if (vratitKod) return { kod: kod, telo: obsah };
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 !== 200) throw new Error('killBottleneck odpověděl ' + kod + ': ' + odpoved.getContentText());
return obsah;
}
/** Najde čísla sloupců podle názvů v prvním řádku. */
function kbNajdiSloupce_(hlavicka) {
const vysledek = {};
Object.keys(SLOUPCE).forEach(function (klic) {
const index = hlavicka.indexOf(SLOUPCE[klic]);
if (index === -1) throw new Error('V tabulce chybí sloupec „' + SLOUPCE[klic] + '".');
vysledek[klic] = index;
});
return vysledek;
}
/**
* Rejstřík uzlů. U každého si pamatuje rodiče a ŘADU, VE KTERÉ STOJÍ RODIČ —
* u kanbanové karty je to přesně ta řada sloupců, takže jde spočítat kolikátý je.
*/
function kbRejstrik_(strom) {
const rejstrik = {};
const projdi = function (uzly, rodic, radaRodice) {
(uzly || []).forEach(function (u) {
rejstrik[u.id] = { uzel: u, rodic: rodic, radaRodice: radaRodice };
projdi(u.children, u, uzly);
});
};
projdi(strom, null, []);
return rejstrik;
}
function kbNajdiPodleNazvu_(uzly, nazev) {
let nalezeny = null;
kbProjdiStrom_(uzly, function (u) { if (!nalezeny && u.title === nazev) nalezeny = u; });
return nalezeny;
}
function kbProjdiStrom_(uzly, akce) {
(uzly || []).forEach(function (u) { akce(u); kbProjdiStrom_(u.children, akce); });
}
/** Hláška uživateli — v tabulce bublina, při automatickém běhu jen do logu. */
function kbHlaska_(text) {
try { SpreadsheetApp.getActiveSpreadsheet().toast(text, 'killBottleneck', 5); }
catch (e) { Logger.log(text); }
}7. Vyplňte nastavení
Nahoře ve skriptu je pět řádků. První tři platí vždycky:
const KB_ADRESA = 'https://vasefirma.killbottleneck.com';
const KB_KLIC = 'kb_user_sem_vlozte_svuj_klic';
const KB_MAPA = 'sem_vlozte_id_mapy';Poslední dva se řídí tím, co jste zvolili v kroku 1:
| Tvar mapy | KB_VETEV | KB_KANBAN |
|---|---|---|
| Stromeček | 'Nové reklamace' | false |
| Nástěnka | 'D1 – Sestavení týmu' | true |
Uložte (💾), nahoře vyberte funkci odeslatNoveReklamace a dejte Spustit. Google se poprvé zeptá na oprávnění — potvrďte ho. V tabulce se u odeslaných řádků objeví id cíle.
8. Stav řešení zpátky do tabulky
Druhý směr už máte taky — je to funkce nacistStavy z téhož skriptu. Načte mapu a ke každému řádku dopíše, kde práce stojí: u stromečku D2 – Popis problému (1/8), u nástěnky sloupec, ve kterém karta právě stojí.
9. Ať to jezdí samo
Zatím se obojí spouští ručně. Automatické spouštění zapnete v Apps Scriptu vlevo pod ikonou budíku (Triggery / Aktivační události) → Přidat aktivační událost:
| Funkce | Zdroj | Typ | K čemu |
|---|---|---|---|
odeslatNoveReklamace | Z tabulky | Při změně | nový řádek putuje do mapy hned |
nacistStavy | Časově řízené | Po hodinách / po minutách | stav řešení se vrací do tabulky |
Po znovuotevření tabulky navíc přibude nahoře nabídka killBottleneck, odkud jde obojí spustit i ručně.
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. Skript 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 podmínka „nadřazený cíl" míří jinam. Zkontrolujte v Pravidla → log běhů. |
| V tabulce chybí sloupec | Skript hledá sloupce podle názvu v prvním řádku. Překlep v hlavičce = chyba. |
| Nic se nestalo a nikde nic není | Aktivační událost se nespustila — zkontrolujte Spuštění (Executions) v levém sloupci Apps Scriptu. |
Co čekat a co ne
Aby vás pak nic nepřekvapilo:
- Termíny jsou po dnech, ne po hodinách. Když máte v předpisu „okamžité opatření do 24 hodin", nejjemnější, co pravidlo umí nastavit, je zítra.
- Pořadí cílů v mapě není pořadí zakládání. Mapa se po přidání přerovná, proto si recept pamatuje id cíle, ne jeho pozici. Nespoléhejte na „poslední v seznamu".
- Pravidla platí jen do budoucna. Reklamace, které v mapě už jsou, se zpětně nerozbalí ani nezačnou posouvat.
- Spuštění pravidel se nepočítá ani neúčtuje, strop je 50 pravidel na mapu.
- killBottleneck nevyrobí hotový 8D formulář pro zákazníka. Hlídá průběh a termíny; dokument, který pošlete odběrateli, si dál veďte tam, kde ho vedete teď.
Kam dál
- Napojení přes n8n — totéž, když máte n8n
- REST API — všechny endpointy, limity a chybové kódy
- Automatizační pravidla — co všechno mapa umí sama

