Lightning Report (n8n)
Automated lightning activity reports for wind farms. An n8n workflow detects strikes via the iklim.co API, generates a Word report through a Python report service, routes it through Slack for human approval, and emails the approved report to customers via SendGrid.
Architecture
┌─────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ n8n workflow│────▶│ iklim.co API │ │ Report service │
│ (scheduled) │ │ lightnings + │ │ FastAPI /generate │
│ │ │ thunderstorms │ │ → DOCX │
└──────┬──────┘ └──────────────────┘ └──────────▲──────────┘
│ │
│ Slack upload + approve/reject buttons │
▼ │
┌─────────────┐ storm_logs datatable (pending report) │
│ Slack │───────────────────────────────────────────┘
└──────┬──────┘
│ on approve
▼
┌─────────────┐
│ SendGrid │──▶ customer contact_email
└─────────────┘
Repository layout
| Path | Description |
|---|---|
Lightning_Report_Automatic.json |
n8n workflow export (import in n8n) |
report_service/ |
FastAPI wrapper around report generation |
report_service/adapter.py |
Maps n8n JSON payload → pandas DataFrames |
src/reporting/docx.py |
Main DOCX builder |
src/analysis/ |
Risk, histogram, geospatial, statistics |
src/visualization/ |
Maps and storm-cell plots |
src/config.py |
Default analysis parameters |
Prerequisites
- Python 3.12+ (local or Docker)
- n8n (self-hosted, with Data Tables enabled)
- iklim.co API credentials (test:
https://api-test.iklim.co) - Slack app/bot for uploads and approval buttons
- SendGrid API key with Mail Send permission
- Optional: Gemini API key if commentary is generated inside the report service (otherwise pass
gemini_textfrom n8n)
Report service (local)
1. Install dependencies
cd Lightning_Report_n8n
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt -r report_service/requirements.txt
2. Environment variables
Create a .env file in the project root (never commit it):
REPORT_SERVICE_TOKEN=your-long-random-secret
GEMINI_API_KEY=optional-if-not-supplied-by-n8n
GEMINI_MODEL=gemini-2.5-flash-lite
LOG_LEVEL=INFO
REPORT_SERVICE_TOKEN is required. The service refuses /generate if it is unset.
3. Run the server
export REPORT_SERVICE_TOKEN="your-long-random-secret"
uvicorn report_service.main:app --host 0.0.0.0 --port 8000
Health check: GET http://localhost:8000/health
4. Expose to n8n (development)
If n8n runs elsewhere, tunnel the service:
ngrok http 8000
Point the n8n Generate Report HTTP Request node at https://<your-ngrok-host>/generate and set header X-Report-Token to the same value as REPORT_SERVICE_TOKEN.
Report service (Docker)
cd Lightning_Report_n8n
export REPORT_SERVICE_TOKEN=your-long-random-secret
docker compose -f report_service/docker-compose.yml up --build -d
From n8n on the same Docker network: http://report-service:8000/generate
API
GET /health
Liveness probe. Returns {"ok": true, ...}.
POST /generate
| Header | Required | Description |
|---|---|---|
X-Report-Token |
Yes | Must match REPORT_SERVICE_TOKEN |
Content-Type |
Yes | application/json |
Body (built by n8n Build Report Payload node):
| Field | Description |
|---|---|
customer_name |
Wind farm / customer label |
t_start, t_end |
Storm window (epoch ms) |
centroid_lat, centroid_lon |
Farm centroid |
boundary_m |
Monitoring radius (m) |
rings |
{ r1, r2, r3, r4 } distance rings |
timezone |
e.g. Europe/Istanbul |
turbines |
Array of turbine objects |
strikes |
Lightning strike records (captured ISO time preferred) |
storm_records |
Thunderstorm polygons from /v1/thunderstorms/within |
gemini_text |
Optional pre-generated commentary |
Response: DOCX file (application/vnd.openxmlformats-officedocument.wordprocessingml.document).
n8n workflow
Import Lightning_Report_Automatic.json into n8n and configure:
Credentials
| Integration | Used for |
|---|---|
| iklim.co login | Login to iklim.co / token refresh |
Slack (n8n_lightning_report_bot) |
Report upload, approval buttons |
Slack (Tarla Slack Account) |
Error / email-sent notifications |
| SendGrid | Customer email after approval |
Data tables
customers
| Column | Purpose |
|---|---|
customer_name |
Display name |
contact_email |
SendGrid recipient |
id |
Linked from wind turbines |
wind_turbine_farm
| Column | Purpose |
|---|---|
customer_id |
FK to customers |
latitude, longitude |
Turbine positions |
name |
Turbine label |
storm_logs
| Column | Purpose |
|---|---|
customer_name, storm_key, storm_start, storm_end |
Storm metadata |
total_strikes, status |
Count; waiting for confirmation → confirmed / rejected |
pending_contact_email |
Email snapshot at report time |
pending_email_file_name |
DOCX filename |
pending_report_base64 |
Queued report (survives n8n restarts) |
pending_report_mime_type |
MIME type for attachment |
Main flow (simplified)
- Daily trigger → authenticate → loop customers.
- Query lightnings within farm boundary; detect storm window.
- Insert/update
storm_logs, fetch thunderstorms, build payload. - Generate Report → upload DOCX to Slack → approval buttons.
- Persist pending report fields on
storm_logs. - On Slack Approve → load queued report → SendGrid email → clear pending fields.
Email is sent only after approval, not when the report is first generated.
Troubleshooting
channel_not_found (Slack)
The Slack channel ID in the node does not exist or the bot is not invited. Re-select the channel in n8n and run /invite @YourBot in that channel.
Account Is not Found For User … (/v1/thunderstorms/within)
Login succeeded but the iklim user has no account on the API environment (e.g. test). Ask iklim.co to provision the user, or use a service account that has thunderstorm access. Lightning-only reports can continue if the workflow skips failed thunderstorm calls (optional branch).
Histogram shows 01-01-1970 / wrong period
Strike timestamps were parsed incorrectly when timestamp (epoch ms) was preferred over captured (ISO). Fixed in report_service/adapter.py — restart the report service after pulling updates.
SendGrid: attachment must be base64 encoded
Use the Prepare SendGrid Email Payload node path; ensure pending_report_base64 in storm_logs is populated and not truncated by datatable size limits.
Binary missing at Upload Report to Slack
The datatable persist step must run after Slack upload, not before (persist nodes drop binary data).
Slack invalid_payload on approval follow-up
Do not double-JSON.stringify the body when posting to response_url. Slack response URLs expire after ~30 minutes (optional confirmation message only).
Development notes
- Per-farm settings (
rings, centroid, dates) come from the n8n payload viaapply_farm_config(), not hardcoded insrc/config.py. - Prefer strike field
captured(ISO) forlocal_time; numerictimestampis supported as epoch milliseconds. - HMAC signing for iklim API calls is implemented in n8n Code nodes (
Calculate Lightning Headers,Calculate Thunderstorm Headers).
License
Internal use — iklim.co lightning reporting pipeline.