
Wir waren auf der Suche nach einer unkomplizierten Lösung, mit der unsere Mitarbeiter Urlaubsanträge digital einreichen können – idealerweise inklusive automatischer Genehmigung, Ablehnung und Kalenderpflege direkt in unserem Google Workspace-Intranet.
Statt gleich in ein teures Tool zu investieren, haben wir überlegt: Geht das nicht auch smarter – und günstiger? Also haben wir selbst einen Chatbot gebaut.
Jetzt können Urlaubsanträge direkt im Google Chat gestellt werden. Vorgesetzte bekommen automatisch eine Benachrichtigung und können mit einem Klick zustimmen oder ablehnen. Wird der Urlaub genehmigt, landet er direkt im Teamkalender – und alle Einträge werden sauber in einem Google Sheet dokumentiert.
Wie einfach das geht? Zeigen wir dir im nächsten Abschnitt.
Klären wir zuerst einmal die Vorteile eines solchen Ansatzes:
Über einfache Slash-Befehle können Nutzer spezifische Aktionen auslösen.
Statt E-Mail-Ketten kann der Bot gezielte Informationen über interaktive Dialogfenster abfragen.
Unsere Mitarbeiter haben unseren Unternehmenschat (Google Chat) eh meisten geöffnet.
Sie müssen keine Tools wechseln. Alle Benachrichtigungen, Auswertungen und auch die Kalendereinträge im gewohnten Umfeld.
Auslöser und Aktionen können leicht verkettet werden. In unserem Urlaubs-Beispiel löst die Einreichung im Chat eine Kette aus: Speichern im Sheet, E-Mail an Manager, Klick auf Genehmigungslink, Status-Update im Sheet, Kalendereintrag, Benachrichtigung an Mitarbeiter. All das ohne manuelles Kopieren, Einfügen oder Nachhaken.
Bevor’s richtig losgeht, brauchst du ein neues Projekt in der Google Cloud Console. Dort aktivierst du die Google Chat API und richtest deinen Bot ein.
Du vergibst einen Namen, lädst ein passendes Profilbild hoch, schreibst eine kurze Beschreibung – und legst fest, auf welche Befehle dein Bot reagieren soll. In unserem Fall sind das:
/help – für die Übersicht der Funktionen/einreichen – zum Stellen eines Urlaubsantrags/uebersicht – um alle bisherigen Anträge einzusehen

Als Nächstes erstellen wir ein Google Apps Script. Das bildet das Herzstück unseres Chatbots.
Besonders wichtig ist dabei die Funktion onMessage(). Sie reagiert auf die Befehle, die wir zuvor im Bot hinterlegt haben (siehe Screenshot).
Damit der Bot auch wirklich hilfreich ist, geben wir am Ende ein JSON-Objekt zurück. Darüber lässt sich z. B. eine übersichtliche Hilfe-Karte im Chat rendern – ideal, um dem Nutzer direkt die nächsten Schritte zu zeigen.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function onMessage(event) {
const message = event.message;
if (message.slashCommand) {
switch (message.slashCommand.commandId) {
case 1: // Hilfe-Befehl
return createHelpCard();
case 2: // Einreichen-Befehl
return openInitialDialog();
case 3: // Übersicht-Befehl
return zeigeUrlaubsUebersicht(event);
case 4: // Stornieren-Befehl
return { text: "Diese Funktion ist noch in Entwicklung." };
}
}
// Wenn kein bekannter Befehl, zeige Hilfe-Karte
return createHelpCard();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
function createHelpCard() {
return {
"cardsV2": [
{
"cardId": "help-card",
"card": {
"sections": [
{
"header": "",
"widgets": [
{
"decoratedText": {
"topLabel": "",
"text": "Hi! 👋 Ich bin hier, um dir mit deinen Urlaubsanträgen zu helfen.<br><br>Hier ist eine Liste von Befehlen, die ich verstehe:",
"wrapText": true
}
}
]
},
{
"widgets": [
{
"decoratedText": {
"topLabel": "",
"text": "<b>/einreichen</b>: Ich reiche einen Urlaubsantrag ein.",
"wrapText": true
}
},
{
"decoratedText": {
"topLabel": "",
"text": "<b>/übersicht</b>: Gib mir eine Übersicht über meinen geplanten Urlaub.",
"wrapText": true
}
}
]
}
],
"header": {
"title": "Urlaubsbot",
"subtitle": "Ich helfe dir deinen Urlaub zu verwalten",
"imageUrl": "https://goo.gle/3SfMkjb",
"imageType": "SQUARE"
}
}
}
]
};
}
Auf ähnliche Weise lassen sich auch interaktive Dialoge im Chat darstellen. In unserem Fall öffnet der Befehl /einreichen ein Dialogfenster, in dem alle relevanten Infos zum Urlaub abgefragt werden – Startdatum, Enddatum, Kommentar etc.
Sobald der Nutzer auf „Einreichen“ klickt, wird automatisch eine E-Mail an die Personalverantwortlichen geschickt – ganz einfach über die Gmail API.
Das Beste: Die Mail muss nicht langweilig sein. Mit etwas HTML lässt sich die Nachricht optisch aufwerten – zum Beispiel mit Buttons zur Genehmigung oder Ablehnung, farblichen Markierungen und einem übersichtlichen Layout.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const DATE_RANGE_FORM_WIDGETS = [
{
"dateTimePicker": {
"name": "dateFrom",
"label": "Von",
"type": "DATE_ONLY",
"valueMsEpoch": new Date(getISODate(0)).getTime() // Aktuelles Datum als Millisekunden seit Epoch
}
},
{
"dateTimePicker": {
"name": "dateTo",
"label": "Bis",
"type": "DATE_ONLY",
"valueMsEpoch": new Date(getISODate(7)).getTime() // 7 Tage später
}
},
{
"textInput": {
"name": "days",
"label": "Anzahl Urlaubstage",
"hintText": "Bitte geben Sie die genaue Anzahl der Urlaubstage an",
"value": ""
}
},
{
"textInput": {
"name": "reason",
"label": "Grund",
"hintText": "Optional: Grund für den Urlaub angeben",
"value": ""
}
}
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const subject = `Urlaubsantrag: ${mitarbeiterName} (${vonDatum} bis ${bisDatum})`;
// E-Mail mit Genehmigungslinks senden
GmailApp.sendEmail(
authorizerEmail,
subject,
`Neuer Urlaubsantrag von ${mitarbeiterName}. Bitte öffne die HTML-Version dieser E-Mail.`,
{
htmlBody: `
<h2>Neuer Urlaubsantrag</h2>
<p><strong>Mitarbeiter:</strong> ${mitarbeiterName} (${aktuellerBenutzer})</p>
<p><strong>Zeitraum:</strong> ${vonDatum} bis ${bisDatum} (${anzahlTage} Tage)</p>
<p><strong>Grund:</strong> ${grund}</p>
<p>
<a href="${ScriptApp.getService().getUrl()}?action=approve&id=${urlaubsId}&name=${encodeURIComponent(mitarbeiterName)}&email=${encodeURIComponent(aktuellerBenutzer)}&startDate=${encodeURIComponent(vonDatum)}&endDate=${encodeURIComponent(bisDatum)}&days=${encodeURIComponent(anzahlTage)}" style="display:inline-block; background-color:#4CAF50; color:white; padding:10px 20px; text-decoration:none; margin-right:10px;">Genehmigen</a>
<a href="${ScriptApp.getService().getUrl()}?action=reject&id=${urlaubsId}" style="display:inline-block; background-color:#f44336; color:white; padding:10px 20px; text-decoration:none;">Ablehnen</a>
</p>
Damit die Buttons in der E-Mail auch wirklich etwas bewirken, muss im Apps Script die Funktion doGet() implementiert werden. Darüber lassen sich Interaktionen abbilden, z. B. wenn ein Vorgesetzter auf „Genehmigen“ klickt.
Mit dem HtmlService können wir dabei sogar eine kleine Bestätigungsseite gestalten.
Im Hintergrund läuft dann Folgendes ab:
1. Über die Calendar API wird der Urlaub direkt in einen für alle sichtbaren Teamkalender eingetragen.
2. Parallel wird über die Spreadsheet API ein neuer Eintrag in einer Google-Tabelle erzeugt. Dort haben Vorgesetzte eine übersichtliche Liste aller Anträge – inklusive Filter- und Suchfunktionen.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function doGet(e) {
try {
// Ausführliche Protokollierung der empfangenen Parameter
console.log("GET-Anfrage erhalten - Alle Parameter:", JSON.stringify(e.parameter));
console.log("Action:", e.parameter.action);
console.log("ID:", e.parameter.id);
const action = e.parameter.action;
const urlaubsId = e.parameter.id;
// Prüfe auf fehlende Parameter und gib detaillierte Fehlermeldung aus
if (!action || !urlaubsId) {
console.log("Fehlende Parameter: action=" + action + ", urlaubsId=" + urlaubsId);
return HtmlService.createHtmlOutput(`
<h2>Fehler: Fehlende Parameter</h2>
<p>Die folgenden Parameter fehlen oder sind ungültig:</p>
<ul>
<li>action: ${action || 'fehlt'}</li>
<li>id: ${urlaubsId || 'fehlt'}</li>
</ul>
<p>Erhaltene Parameter: ${JSON.stringify(e.parameter)}</p>
`);
}
if (action === "approve") {
// Parameter für die Genehmigung
const mitarbeiterName = e.parameter.name || "";
const mitarbeiterEmail = e.parameter.email || "";
const vonDatum = e.parameter.startDate || "";
const bisDatum = e.parameter.endDate || "";
const anzahlTage = e.parameter.days || "";
// Status aktualisieren
aktualisiereUrlaubsantragStatus(urlaubsId, "Genehmigt");
// Kalendereintrag erstellen
try {
const calendarEvent = createCalendarEvent(vonDatum, bisDatum, mitarbeiterName, anzahlTage);
console.log("Kalendereintrag erstellt: " + (calendarEvent ? calendarEvent.getId() : "Fehler"));
} catch (calendarError) {
console.error("Fehler beim Erstellen des Kalendereintrags:", calendarError);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Kalender-ID aus den Eigenschaften holen
const calendarId = PropertiesService.getScriptProperties().getProperty('URLAUB_KALENDER_ID') ||
'primary'; // Fallback auf den Hauptkalender
// Erstelle Kalendereintrag
const calendarEvent = CalendarApp.getCalendarById(calendarId).createAllDayEvent(
`${mitarbeiterName} - Urlaub`,
startDate,
endDate
);
// Weitere Details zum Kalendereintrag hinzufügen
calendarEvent.setDescription(`Genehmigter Urlaub für ${mitarbeiterName} (${anzahlTage} Tage)`);
calendarEvent.setColor(CalendarApp.EventColor.PALE_BLUE); // Farbkodierung für Urlaub1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const SPREADSHEET_ID = PropertiesService.getScriptProperties().getProperty('URLAUB_SHEET_ID');
if (!SPREADSHEET_ID) {
console.log("Kein Spreadsheet konfiguriert, überspringe Speicherung");
return;
}
const spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);
const sheet = spreadsheet.getSheetByName('Urlaubsanträge') || spreadsheet.insertSheet('Urlaubsanträge');
// Stelle sicher, dass die Header existieren
if (sheet.getLastRow() === 0) {
sheet.appendRow([
'ID', 'Datum eingereicht', 'Mitarbeiter', 'Email',
'Von', 'Bis', 'Tage', 'Grund', 'Status'
]);
}
// Füge den neuen Antrag hinzu
sheet.appendRow([
urlaubsId,
new Date().toLocaleDateString('de-DE'),
name,
email,
vonDatum,
bisDatum,
anzahlTage,
grund,
'Ausstehend'
]);
Damit ist die Implementierung abgeschlossen. Jetzt muss das Ganze nur noch mit einem Klick als Web-App deployed werden – und schon ist der Bot einsatzbereit.
Unsere Mitarbeitenden sind auf jeden Fall begeistert von der Lösung. Und wir sind sicher: Das war nicht das letzte Mal, dass wir unseren Google Workspace programmatisch erweitert haben.