Files
monthly-report-kit/n8n/workflow-template.json
T
2026-06-24 14:40:43 +00:00

208 lines
16 KiB
JSON

{
"name": "Monatsreport - Startertemplate",
"nodes": [
{
"parameters": {},
"id": "bce6507d-0a40-4c72-a054-3f3860f96601",
"name": "Manual Trigger",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-760,
0
]
},
{
"parameters": {
"jsCode": "const sample = {\"report\": {\"report_version\": \"1.0\", \"prompt_version\": \"1.0\", \"template_version\": \"1.0\", \"generated_at\": \"2026-06-24T14:30:00+02:00\", \"publisher\": \"Fewo Performance Studio\", \"period\": {\"year\": 2026, \"month\": 5, \"label\": \"Mai 2026\", \"comparison_label\": \"April 2026\"}}, \"customer\": {\"id\": \"kuestenblick-001\", \"name\": \"Ferienapartments Kuestenblick\", \"property_name\": \"Apartment Seestern\", \"location\": \"Nordseekueste\", \"contact_line\": \"reporting@kuestenblick.example\", \"logo_data_uri\": \"data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxNjAgMTYwJz48cmVjdCB3aWR0aD0nMTYwJyBoZWlnaHQ9JzE2MCcgcng9JzM1JyBmaWxsPScjZmZmZmZmJy8+PHBhdGggZD0nTTI4IDEwNWMyOC0xMCA1NC0xMCAxMDQgMCcgc3Ryb2tlPScjMWY1ZDY2JyBzdHJva2Utd2lkdGg9JzknIGZpbGw9J25vbmUnIHN0cm9rZS1saW5lY2FwPSdyb3VuZCcvPjxjaXJjbGUgY3g9JzgwJyBjeT0nNjInIHI9JzI4JyBmaWxsPScjZGJhMjVkJy8+PHBhdGggZD0nTTQ4IDYyaDY0JyBzdHJva2U9JyMxZjVkNjYnIHN0cm9rZS13aWR0aD0nOCcgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJy8+PC9zdmc+\", \"logo_text\": \"KB\"}, \"design\": {\"theme\": \"modern\", \"primary_color\": \"#1F5D66\", \"secondary_color\": \"#2C7C82\", \"accent_color\": \"#DBA25D\", \"text_color\": \"#24343A\", \"muted_color\": \"#718087\", \"surface_color\": \"#F2F6F5\", \"font_family\": \"Arial\"}, \"metrics\": {\"currency\": \"EUR\", \"revenue\": 6840.5, \"occupancy_rate\": 78.4, \"adr\": 126.68, \"revpar\": 99.32, \"bookings\": 19, \"nights_sold\": 54, \"average_stay\": 3.6, \"cancellation_rate\": 6.2, \"comparison\": {\"revenue_delta_pct\": 12.8, \"occupancy_delta_pp\": 5.7, \"adr_delta_pct\": 4.6, \"bookings_delta\": 2}}, \"trends\": [{\"label\": \"Dez 25\", \"revenue\": 4210.0, \"occupancy_rate\": 55.2, \"adr\": 112.3}, {\"label\": \"Jan 26\", \"revenue\": 3890.0, \"occupancy_rate\": 51.4, \"adr\": 109.85}, {\"label\": \"Feb 26\", \"revenue\": 4475.0, \"occupancy_rate\": 59.7, \"adr\": 114.2}, {\"label\": \"Mär 26\", \"revenue\": 5360.0, \"occupancy_rate\": 66.1, \"adr\": 118.75}, {\"label\": \"Apr 26\", \"revenue\": 6064.0, \"occupancy_rate\": 72.7, \"adr\": 121.1}, {\"label\": \"Mai 26\", \"revenue\": 6840.5, \"occupancy_rate\": 78.4, \"adr\": 126.68}], \"content\": {\"summary\": {\"headline\": \"Starker Umsatzmonat bei weiterhin gesunder Auslastung\", \"text\": \"Der Mai verbindet eine hohe Auslastung mit einem gestiegenen durchschnittlichen Tagespreis. Dadurch liegt der Umsatz klar ueber dem Vormonat, ohne dass die Buchungszahl im gleichen Umfang wachsen musste. Die Stornoquote bleibt beherrschbar. Fuer die kommenden Wochen sollte der Fokus deshalb auf einer feineren Preissteuerung an besonders nachgefragten Tagen liegen.\", \"rating\": \"positive\"}, \"developments\": [{\"title\": \"Umsatz gewinnt ueberproportional\", \"text\": \"Der Umsatz steigt staerker als die Buchungszahl. Das spricht dafuer, dass der hoehere Tagespreis und eine bessere Belegung der verfuegbaren Naechte den Monat getragen haben.\", \"impact\": \"positive\"}, {\"title\": \"Auslastung erreicht ein solides Niveau\", \"text\": \"Mit 78,4 Prozent ist ein grosser Teil der verfuegbaren Kapazitaet verkauft. Gleichzeitig verbleibt genug Spielraum, um einzelne schwache Wochentage gezielt zu stimulieren.\", \"impact\": \"positive\"}, {\"title\": \"Stornos bleiben ein Beobachtungspunkt\", \"text\": \"Die Stornoquote ist nicht kritisch, sollte aber nach Buchungskanal und Vorlaufzeit beobachtet werden. So laesst sich erkennen, ob bestimmte Tarife oder Plattformen ueberdurchschnittlich betroffen sind.\", \"impact\": \"neutral\"}, {\"title\": \"Aufenthaltsdauer bleibt kompakt\", \"text\": \"Die durchschnittliche Aufenthaltsdauer von 3,6 Naechten sorgt fuer regelmaessige Gaestewechsel. Das kann Umsatzchancen schaffen, erhoeht jedoch zugleich den operativen Aufwand je belegter Nacht.\", \"impact\": \"neutral\"}], \"recommendations\": [{\"priority\": 1, \"category\": \"Preisgestaltung\", \"title\": \"Nachfragestarke Wochenenden differenzierter bepreisen\", \"reason\": \"Umsatz, Tagespreis und Auslastung entwickeln sich gleichzeitig positiv. Dadurch besteht an stark nachgefragten Terminen Spielraum, den Preis vorsichtig weiter auszureizen.\", \"action\": \"Fuer Freitage, Samstage und lokale Spitzentermine einen Preisaufschlag von zunaechst 5 Prozent testen und die Buchungsgeschwindigkeit wochenweise kontrollieren.\", \"expected_effect\": \"Hoeherer Umsatz pro verfuegbarer Nacht ohne pauschale Preiserhoehung fuer den gesamten Monat.\"}, {\"priority\": 2, \"category\": \"Verfuegbarkeit & Buchungssteuerung\", \"title\": \"Schwache Wochentage mit Mindestaufenthalten entlasten\", \"reason\": \"Die Auslastung ist insgesamt gut, dennoch koennen einzelne Luecken zwischen kurzen Aufenthalten entstehen und wertvolle Naechte blockieren.\", \"action\": \"Fuer ausgewaehlte Anreisetage flexible Mindestaufenthalte einsetzen und kurze Restluecken gezielt fuer Zwei-Nacht-Buchungen freigeben.\", \"expected_effect\": \"Weniger unverkaeufliche Einzelnächte und eine gleichmaessigere Belegung.\"}, {\"priority\": 3, \"category\": \"Gaeste & Bewertungen\", \"title\": \"Stornierungen nach Kanal und Vorlauf auswerten\", \"reason\": \"Die aktuelle Stornoquote ist tragbar, liefert ohne Segmentierung aber noch keinen Hinweis auf die eigentliche Ursache.\", \"action\": \"Stornos kuenftig nach Buchungskanal, Tarifart und Tagen bis zur Anreise gruppieren und im naechsten Report separat vergleichen.\", \"expected_effect\": \"Gezieltere Tarifregeln und weniger vermeidbare kurzfristige Ausfaelle.\"}], \"outlook\": {\"headline\": \"Gute Ausgangslage fuer den Fruehsommer\", \"text\": \"Die Kombination aus stabiler Nachfrage und verbessertem Tagespreis schafft eine robuste Basis fuer Juni. Entscheidend ist, Preiserhoehungen nur an Tagen mit erkennbar hoher Nachfrage einzusetzen und freie Randtage nicht versehentlich mit zu strengen Aufenthaltsregeln zu blockieren.\"}, \"data_quality_notes\": [\"Die Bewertung basiert auf Monatsaggregaten; Unterschiede zwischen Wochentagen und Buchungskanaelen sind darin noch nicht getrennt sichtbar.\"]}, \"legal_note\": \"Dieser Report ist eine datenbasierte Entscheidungshilfe. Prognosen und erwartete Effekte sind keine Garantie fuer zukuenftige Ergebnisse.\"};\nreturn [{ json: { customer: sample.customer, period: sample.report.period, metrics: sample.metrics, trends: sample.trends, design: sample.design, publisher: sample.report.publisher, legal_note: sample.legal_note } }];"
},
"id": "049ac065-2b1f-4ae6-815d-29c44a34df01",
"name": "Monatsdaten vorbereiten",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-540,
0
]
},
{
"parameters": {
"jsCode": "// n8n Code Node: \"Claude Request bauen\"\n// Erwartet: customer, period, metrics, trends und optional context.\n// Das Schema ist fuer die rohe Messages API vereinfacht. Die strengeren\n// Laengen- und Wertepruefungen erfolgen anschliessend im Report Renderer.\n\nconst input = $json;\nconst schema = {\"type\": \"object\", \"additionalProperties\": false, \"properties\": {\"summary\": {\"type\": \"object\", \"additionalProperties\": false, \"properties\": {\"headline\": {\"type\": \"string\", \"description\": \"Minimum characters: 8. Maximum characters: 110.\"}, \"text\": {\"type\": \"string\", \"description\": \"Minimum characters: 80. Maximum characters: 900.\"}, \"rating\": {\"type\": \"string\", \"enum\": [\"positive\", \"neutral\", \"watch\"]}}, \"required\": [\"headline\", \"text\", \"rating\"]}, \"developments\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"additionalProperties\": false, \"properties\": {\"text\": {\"type\": \"string\", \"description\": \"Minimum characters: 40. Maximum characters: 450.\"}, \"impact\": {\"type\": \"string\", \"enum\": [\"positive\", \"neutral\", \"negative\"]}}, \"required\": [\"title\", \"text\", \"impact\"]}, \"description\": \"Minimum items: 2. Maximum items: 4.\"}, \"recommendations\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"additionalProperties\": false, \"properties\": {\"priority\": {\"type\": \"integer\", \"description\": \"Minimum value: 1. Maximum value: 3.\"}, \"category\": {\"type\": \"string\", \"description\": \"Minimum characters: 3. Maximum characters: 80.\"}, \"reason\": {\"type\": \"string\", \"description\": \"Minimum characters: 40. Maximum characters: 450.\"}, \"action\": {\"type\": \"string\", \"description\": \"Minimum characters: 30. Maximum characters: 450.\"}, \"expected_effect\": {\"type\": \"string\", \"description\": \"Minimum characters: 20. Maximum characters: 300.\"}}, \"required\": [\"priority\", \"category\", \"title\", \"reason\", \"action\", \"expected_effect\"]}, \"description\": \"Minimum items: 1. Maximum items: 3.\"}, \"outlook\": {\"type\": \"object\", \"additionalProperties\": false, \"properties\": {\"headline\": {\"type\": \"string\", \"description\": \"Minimum characters: 5. Maximum characters: 100.\"}, \"text\": {\"type\": \"string\", \"description\": \"Minimum characters: 60. Maximum characters: 700.\"}}, \"required\": [\"headline\", \"text\"]}, \"data_quality_notes\": {\"type\": \"array\", \"items\": {\"type\": \"string\", \"description\": \"Minimum characters: 5. Maximum characters: 240.\"}, \"description\": \"Maximum items: 4.\"}}, \"required\": [\"summary\", \"developments\", \"recommendations\", \"outlook\", \"data_quality_notes\"]};\n\nconst systemPrompt = `Du erstellst die redaktionellen Inhalte eines monatlichen Performance-Reports fuer Betreiber von Ferienunterkuenften.\nNutze ausschliesslich die gelieferten Daten. Erfinde keine Zahlen oder Ursachen.\nJede Empfehlung muss konkret und aus den Daten ableitbar sein.\nFormuliere professionell, klar und auf Deutsch. Vermeide Marketingfloskeln und Wiederholungen.\nErzeuge 2 bis 4 Entwicklungen, 1 bis 3 Empfehlungen und hoechstens 4 Datenhinweise.\nNutze data_quality_notes fuer fehlende oder eingeschraenkte Datengrundlagen.`;\n\nconst userPrompt = `Erstelle den Monatsreport fuer diese Daten:\n\n${JSON.stringify({\n customer: input.customer,\n period: input.period,\n metrics: input.metrics,\n trends: input.trends,\n context: input.context ?? {}\n}, null, 2)}`;\n\nreturn [{\n json: {\n ...input,\n claudeRequest: {\n model: input.claudeModel ?? 'claude-sonnet-4-6',\n max_tokens: 3000,\n temperature: 0.2,\n system: systemPrompt,\n messages: [{ role: 'user', content: userPrompt }],\n output_config: {\n format: { type: 'json_schema', schema }\n }\n }\n }\n}];\n"
},
"id": "6a1c5321-97ac-453f-80b5-0f6782d12103",
"name": "Claude Request bauen",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-300,
0
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.anthropic.com/v1/messages",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "x-api-key",
"value": "={{ $env.ANTHROPIC_API_KEY }}"
},
{
"name": "anthropic-version",
"value": "2023-06-01"
},
{
"name": "content-type",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify($json.claudeRequest) }}",
"options": {}
},
"id": "8efe292a-6210-4ea7-8d4d-5d2c009e2de2",
"name": "Claude Report erzeugen",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-40,
0
]
},
{
"parameters": {
"jsCode": "// n8n Code Node: \"Claude JSON extrahieren\"\n// Erwartet die Antwort der Anthropic Messages API.\n\nconst response = $json;\n\nif (response.stop_reason === 'refusal') {\n throw new Error('Claude hat die Anfrage abgelehnt. Es wird kein Report erzeugt.');\n}\nif (response.stop_reason === 'max_tokens') {\n throw new Error('Claude-Ausgabe wurde wegen max_tokens abgeschnitten. max_tokens erhoehen und erneut ausfuehren.');\n}\nconst textBlock = Array.isArray(response.content)\n ? response.content.find(block => block.type === 'text')\n : null;\n\nif (!textBlock?.text) {\n throw new Error('Anthropic-Antwort enthaelt keinen Textblock.');\n}\n\nlet content;\ntry {\n content = JSON.parse(textBlock.text);\n} catch (error) {\n throw new Error(`Claude-Ausgabe ist kein valides JSON: ${error.message}`);\n}\n\nreturn [{\n json: {\n content,\n claudeMeta: {\n id: response.id,\n model: response.model,\n stop_reason: response.stop_reason,\n usage: response.usage\n }\n }\n}];\n"
},
"id": "b254c588-e563-424e-b43c-0607b48a029a",
"name": "Claude JSON extrahieren",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
210,
0
]
},
{
"parameters": {
"jsCode": "// n8n Code Node: \"Render Payload bauen\"\n// Passe die Node-Namen unten an deinen Workflow an.\n\nconst source = $('Monatsdaten vorbereiten').first().json;\nconst claude = $('Claude JSON extrahieren').first().json;\n\nconst now = new Date().toISOString();\n\nconst payload = {\n report: {\n report_version: '1.0',\n prompt_version: '1.0',\n template_version: '1.0',\n generated_at: now,\n publisher: source.publisher ?? 'Mein Reporting',\n period: source.period\n },\n customer: source.customer,\n design: source.design,\n metrics: source.metrics,\n trends: source.trends,\n content: claude.content,\n legal_note: source.legal_note ?? 'Dieser Report ist eine datenbasierte Entscheidungshilfe und keine Garantie fuer zukuenftige Ergebnisse.'\n};\n\nreturn [{ json: payload }];\n"
},
"id": "b6b414aa-c65d-4ce4-8147-7508ab63cc04",
"name": "Render Payload bauen",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
460,
0
]
},
{
"parameters": {
"method": "POST",
"url": "http://report-renderer:8080/render",
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify($json) }}",
"options": {
"response": {
"response": {
"responseFormat": "file",
"outputPropertyName": "data"
}
}
}
},
"id": "166260b5-0b58-421c-ac87-6696c293968c",
"name": "PDF rendern",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
710,
0
]
}
],
"connections": {
"Manual Trigger": {
"main": [
[
{
"node": "Monatsdaten vorbereiten",
"type": "main",
"index": 0
}
]
]
},
"Monatsdaten vorbereiten": {
"main": [
[
{
"node": "Claude Request bauen",
"type": "main",
"index": 0
}
]
]
},
"Claude Request bauen": {
"main": [
[
{
"node": "Claude Report erzeugen",
"type": "main",
"index": 0
}
]
]
},
"Claude Report erzeugen": {
"main": [
[
{
"node": "Claude JSON extrahieren",
"type": "main",
"index": 0
}
]
]
},
"Claude JSON extrahieren": {
"main": [
[
{
"node": "Render Payload bauen",
"type": "main",
"index": 0
}
]
]
},
"Render Payload bauen": {
"main": [
[
{
"node": "PDF rendern",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"active": false,
"versionId": "d6a55fb4-d389-48de-9ba5-4e63c6e0d552",
"meta": {
"templateCredsSetupCompleted": false
},
"tags": []
}