AutomationFlowsGeneral › Dev Activity Reporter

Dev Activity Reporter

dev_activity_reporter. Uses dataTable, emailSend. Scheduled trigger; 19 nodes.

Cron / scheduled trigger★★★★☆ complexity19 nodesData TableEmail Send
General Trigger: Cron / scheduled Nodes: 19 Complexity: ★★★★☆ Added:

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "name": "dev_activity_reporter",
  "nodes": [
    {
      "parameters": {
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "value": "61EXjt0nLCKmZ7ON",
          "mode": "list",
          "cachedResultName": "dev_activity_daemon",
          "cachedResultUrl": "/projects/57QGut0RrnCgx05p/datatables/61EXjt0nLCKmZ7ON"
        },
        "matchType": "allConditions",
        "filters": {
          "conditions": [
            {
              "keyName": "start_time",
              "condition": "gt",
              "keyValue": "={{ $now.minus({days: 7}) }}"
            }
          ]
        },
        "returnAll": true,
        "orderBy": true
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        160,
        464
      ],
      "id": "9d47f96e-fb80-4160-adb8-79212f7ddba7",
      "name": "Weekly getter"
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtHour": 22
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        -48,
        464
      ],
      "id": "90ac5615-e02d-44fb-b1ba-89095d1d3062",
      "name": "Weekly trigger"
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "months"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        -48,
        704
      ],
      "id": "23b00db0-340b-43b5-ad60-a8c8c3d8444c",
      "name": "Monthly trigger"
    },
    {
      "parameters": {
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "value": "61EXjt0nLCKmZ7ON",
          "mode": "list",
          "cachedResultName": "dev_activity_daemon",
          "cachedResultUrl": "/projects/57QGut0RrnCgx05p/datatables/61EXjt0nLCKmZ7ON"
        },
        "matchType": "allConditions",
        "filters": {
          "conditions": [
            {
              "keyName": "start_time",
              "condition": "lt",
              "keyValue": "={{ $now.startOf('month') }}"
            },
            {
              "keyName": "start_time",
              "condition": "gt",
              "keyValue": "={{ $now.minus({months: 1}).startOf('month') }}"
            }
          ]
        },
        "returnAll": true,
        "orderBy": true
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        160,
        704
      ],
      "id": "aaf33ad7-4b4b-49e1-b57a-47163c63bfdf",
      "name": "Monthly getter"
    },
    {
      "parameters": {
        "language": "pythonNative",
        "pythonCode": "from datetime import datetime, timezone\nimport pandas as pd\n\n# --- Receive rows from Data Tables node ---\nrows = [item['json'] for item in _items]\ndf = pd.DataFrame(rows)\n\n# --- Parse and clean ---\ndf[\"start_time\"] = pd.to_datetime(df[\"start_time\"], utc=True)\ndf[\"end_time\"] = pd.to_datetime(df[\"end_time\"], utc=True)\ndf[\"duration_seconds\"] = pd.to_numeric(df[\"duration_seconds\"], errors=\"coerce\").fillna(0)\ndf[\"is_idle\"] = df[\"is_idle\"].astype(bool)\ndf[\"category\"] = df[\"category\"].fillna(\"Uncategorized\")\n\n# --- Split active vs idle ---\ndf_active = df[df[\"is_idle\"] == False].copy()\ndf_idle = df[df[\"is_idle\"] == True].copy()\n\ntotal_active = int(df_active[\"duration_seconds\"].sum())\ntotal_idle = int(df_idle[\"duration_seconds\"].sum())\n\n# --- 1. Time per category ---\nby_category = (\n    df_active.groupby(\"category\")[\"duration_seconds\"]\n    .sum()\n    .sort_values(ascending=False)\n    .astype(int)\n    .to_dict()\n)\n\n# --- 2. Daily activity ---\ndf_active[\"day\"] = df_active[\"start_time\"].dt.strftime(\"%a %d.%m\")\nby_day = (\n    df_active.groupby(\"day\")[\"duration_seconds\"]\n    .sum()\n    .astype(int)\n    .to_dict()\n)\n\n# --- 3. Top 10 apps + Others (for pie chart) ---\napp_totals = (\n    df_active.groupby(\"window_title\")[\"duration_seconds\"]\n    .sum()\n    .sort_values(ascending=False)\n)\ntop_10 = app_totals.head(10).astype(int).to_dict()\nothers_sum = int(app_totals.iloc[10:].sum())\nif others_sum > 0:\n    top_10[\"Others\"] = others_sum\ntop_apps_pie = top_10\n\n# --- 4. Heatmap: weekday vs hour ---\ndf_active[\"hour\"] = df_active[\"start_time\"].dt.hour\ndf_active[\"weekday\"] = df_active[\"start_time\"].dt.strftime(\"%a\")\nheatmap = (\n    df_active.groupby([\"weekday\", \"hour\"])[\"duration_seconds\"]\n    .sum()\n    .reset_index()\n    .rename(columns={\"duration_seconds\": \"seconds\"})\n    .astype({\"seconds\": int})\n    .to_dict(orient=\"records\")\n)\n\n# --- 5. Stacked bar: category time per day ---\nWEEKDAY_ORDER = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"]\ndf_active[\"weekday_short\"] = df_active[\"start_time\"].dt.strftime(\"%a\")\nstacked = (\n    df_active.groupby([\"weekday_short\", \"category\"])[\"duration_seconds\"]\n    .sum()\n    .reset_index()\n)\n# Build dict: {category: {day: hours}}\nall_categories = stacked[\"category\"].unique().tolist()\nstacked_by_category = {}\nfor cat in all_categories:\n    cat_df = stacked[stacked[\"category\"] == cat]\n    day_dict = dict(zip(cat_df[\"weekday_short\"], cat_df[\"duration_seconds\"] / 3600))\n    stacked_by_category[cat] = {\n        day: round(day_dict.get(day, 0.0), 2)\n        for day in WEEKDAY_ORDER\n    }\n\nreturn [{\n    \"json\": {\n        \"total_active_seconds\": total_active,\n        \"total_idle_seconds\": total_idle,\n        \"by_category\": by_category,\n        \"by_day\": by_day,\n        \"top_apps_pie\": top_apps_pie,\n        \"stacked_by_category\": stacked_by_category,\n        \"heatmap\": heatmap,\n        \"weekday_order\": WEEKDAY_ORDER,\n        \"all_categories\": all_categories,\n    }\n}]"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        368,
        464
      ],
      "id": "fe4c6bd3-c465-44ed-9f14-3db236fe049b",
      "name": "Week analysis"
    },
    {
      "parameters": {
        "language": "pythonNative",
        "pythonCode": "import json\nimport urllib.parse\nimport urllib.request\n\ndata = _items[0]['json']\n\nCOLORS = [\n    '#4C9BE8', '#E87B4C', '#4CE87B', '#E84C9B',\n    '#9B4CE8', '#E8E84C', '#4CE8E8', '#E84C4C',\n    '#9BE84C', '#4C4CE8', '#888888'\n]\n\nBASE_URL = \"https://quickchart.io/chart?w=600&h=300&bkg=%231a1a2e&c=\"\n\ndef make_short_url(config):\n    payload = json.dumps({\"chart\": config, \"width\": 600, \"height\": 300, \"backgroundColor\": \"#1a1a2e\"}).encode('utf-8')\n    req = urllib.request.Request(\n        \"https://quickchart.io/chart/create\",\n        data=payload,\n        headers={\"Content-Type\": \"application/json\"},\n        method=\"POST\"\n    )\n    with urllib.request.urlopen(req, timeout=10) as resp:\n        result = json.loads(resp.read().decode())\n        return result[\"url\"]\n\ndef make_url(config):\n    encoded = urllib.parse.quote(json.dumps(config))\n    return BASE_URL + encoded\n\ndef sec_to_hm(s):\n    h = int(s // 3600)\n    m = int((s % 3600) // 60)\n    return f\"{h}h {m}m\"\n\n# --- Chart 1: Time per category (horizontal bar) ---\ncat_items = [(k, round(v / 3600, 2)) for k, v in data['by_category'].items() if v > 0]\ncats = [x[0] for x in cat_items]\nvals = [x[1] for x in cat_items]\n\nchart_category = make_short_url({\n    \"type\": \"horizontalBar\",\n    \"data\": {\n        \"labels\": cats,\n        \"datasets\": [{\n            \"label\": \"Hours\",\n            \"data\": vals,\n            \"backgroundColor\": COLORS[:len(cats)]\n        }]\n    },\n    \"options\": {\n        \"legend\": {\"labels\": {\"fontColor\": \"white\"}},\n        \"scales\": {\n            \"xAxes\": [{\"ticks\": {\"fontColor\": \"white\"}, \"gridLines\": {\"color\": \"#333\"}}],\n            \"yAxes\": [{\"ticks\": {\"fontColor\": \"white\"}, \"gridLines\": {\"color\": \"#333\"}}]\n        }\n    }\n})\n\n# --- Chart 2: Daily activity bar ---\ndays = data['weekday_order']\nby_day_short = {}\nfor k, v in data['by_day'].items():\n    short = k.split(' ')[0]\n    by_day_short[short] = by_day_short.get(short, 0) + v\nday_vals = [round(by_day_short.get(d, 0) / 3600, 2) for d in days]\n\nchart_daily = make_url({\n    \"type\": \"bar\",\n    \"data\": {\n        \"labels\": days,\n        \"datasets\": [{\n            \"label\": \"Hours\",\n            \"data\": day_vals,\n            \"backgroundColor\": \"#4C9BE8\"\n        }]\n    },\n    \"options\": {\n        \"legend\": {\"labels\": {\"fontColor\": \"white\"}},\n        \"scales\": {\n            \"xAxes\": [{\"ticks\": {\"fontColor\": \"white\"}, \"gridLines\": {\"color\": \"#333\"}}],\n            \"yAxes\": [{\"ticks\": {\"fontColor\": \"white\"}, \"gridLines\": {\"color\": \"#333\"}}]\n        }\n    }\n})\n\n# --- Chart 3: Top apps pie ---\napp_labels = list(data['top_apps_pie'].keys())\napp_vals_seconds = list(data['top_apps_pie'].values())\napp_vals = [round(v / 60) for v in app_vals_seconds]  # minutes for chart proportions\n\nshort_labels = [\n    (l[:30] + '...' if len(l) > 30 else l) + f\"  [{sec_to_hm(v)}]\"\n    for l, v in zip(app_labels, app_vals_seconds)\n]\n\ntotal_app_secs = sum(app_vals_seconds)\nshort_labels = [\n    (l[:25] + '...' if len(l) > 25 else l)\n    + f\"  [{round(v / total_app_secs * 100, 1)}%  {sec_to_hm(v)}]\"\n    for l, v in zip(app_labels, app_vals_seconds)\n]\n\nchart_top_apps = make_short_url({\n    \"type\": \"pie\",\n    \"data\": {\n        \"labels\": short_labels,\n        \"datasets\": [{\n            \"data\": app_vals,\n            \"backgroundColor\": COLORS[:len(app_vals)]\n        }]\n    },\n    \"options\": {\n        \"legend\": {\n            \"position\": \"right\",\n            \"labels\": {\"fontColor\": \"white\", \"fontSize\": 10}\n        }\n    }\n})\n\n# --- Chart 4: Stacked bar category per day ---\ncategories = data['all_categories']\ndatasets = []\nfor i, cat in enumerate(categories):\n    cat_vals = [round(data['stacked_by_category'].get(cat, {}).get(d, 0), 2) for d in days]\n    datasets.append({\n        \"label\": cat,\n        \"data\": cat_vals,\n        \"backgroundColor\": COLORS[i % len(COLORS)]\n    })\n\nchart_stacked = make_url({\n    \"type\": \"bar\",\n    \"data\": {\n        \"labels\": days,\n        \"datasets\": datasets\n    },\n    \"options\": {\n        \"scales\": {\n            \"xAxes\": [{\"stacked\": True, \"ticks\": {\"fontColor\": \"white\"}, \"gridLines\": {\"color\": \"#333\"}}],\n            \"yAxes\": [{\"stacked\": True, \"ticks\": {\"fontColor\": \"white\"}, \"gridLines\": {\"color\": \"#333\"}}]\n        },\n        \"legend\": {\"labels\": {\"fontColor\": \"white\"}}\n    }\n})\n\n# --- Chart 5: Active vs Idle doughnut ---\nactive_h = round(data['total_active_seconds'] / 3600, 2)\nidle_h = round(data['total_idle_seconds'] / 3600, 2)\nif active_h == 0 and idle_h == 0:\n    idle_h = 0.01\n\ntotal_h = active_h + idle_h\nactive_pct = round(100 * active_h / total_h, 1) if total_h > 0 else 0\nidle_pct = round(100 * idle_h / total_h, 1) if total_h > 0 else 0\n\nchart_idle = make_short_url({\n    \"type\": \"doughnut\",\n    \"data\": {\n        \"labels\": [\n            f\"Active {active_pct}%  ({sec_to_hm(data['total_active_seconds'])})\",\n            f\"Idle {idle_pct}%  ({sec_to_hm(data['total_idle_seconds'])})\"\n        ],\n        \"datasets\": [{\n            \"data\": [active_h, idle_h],\n            \"backgroundColor\": [\"#4C9BE8\", \"#E84C4C\"]\n        }]\n    },\n    \"options\": {\n        \"legend\": {\n            \"position\": \"bottom\",\n            \"labels\": {\"fontColor\": \"white\"}\n        }\n    }\n})\n\n# --- Chart 6: Heatmap (bar per hour) ---\nhour_sums = {}\nfor entry in data['heatmap']:\n    h = entry['hour']\n    hour_sums[h] = hour_sums.get(h, 0) + entry['seconds']\n\nhours = list(range(24))\nhour_vals = [round(hour_sums.get(h, 0) / 60, 1) for h in hours]  # minutes\n\nchart_heatmap = make_url({\n    \"type\": \"bar\",\n    \"data\": {\n        \"labels\": [f\"{h:02d}:00\" for h in hours],\n        \"datasets\": [{\n            \"label\": \"Activity (minutes)\",\n            \"data\": hour_vals,\n            \"backgroundColor\": \"#4C9BE8\"\n        }]\n    },\n    \"options\": {\n        \"title\": {\n            \"display\": True,\n            \"text\": \"Activity by Hour of Day\",\n            \"fontColor\": \"white\"\n        },\n        \"legend\": {\"labels\": {\"fontColor\": \"white\"}},\n        \"scales\": {\n            \"xAxes\": [{\"ticks\": {\"fontColor\": \"white\", \"maxRotation\": 45}, \"gridLines\": {\"color\": \"#333\"}}],\n            \"yAxes\": [{\"ticks\": {\"fontColor\": \"white\"}, \"gridLines\": {\"color\": \"#333\"},\n                       \"scaleLabel\": {\"display\": True, \"labelString\": \"Minutes\", \"fontColor\": \"white\"}}]\n        }\n    }\n})\n\nreturn [{'json': {\n    'chart_category': chart_category,\n    'chart_daily': chart_daily,\n    'chart_top_apps': chart_top_apps,\n    'chart_stacked': chart_stacked,\n    'chart_idle': chart_idle,\n    'chart_heatmap': chart_heatmap,\n    'total_active_seconds': data['total_active_seconds'],\n    'total_idle_seconds': data['total_idle_seconds'],\n    'by_category': data['by_category'],\n    'weekday_order': data['weekday_order'],\n    'all_categories': data['all_categories'],\n}}]"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        576,
        464
      ],
      "id": "dc37ee95-9825-4173-ae99-9d33e460f2a6",
      "name": "Graphs creator"
    },
    {
      "parameters": {
        "language": "pythonNative",
        "pythonCode": "data = _items[0]['json']\n\ndef seconds_to_hm(s):\n    h = int(s // 3600)\n    m = int((s % 3600) // 60)\n    return f\"{h}h {m}m\"\n\ntotal_active = seconds_to_hm(data['total_active_seconds'])\ntotal_idle = seconds_to_hm(data['total_idle_seconds'])\n\ncategory_rows = \"\"\nfor cat, secs in data['by_category'].items():\n    category_rows += f\"<tr><td style='padding:6px 12px;color:#ccc;'>{cat}</td><td style='padding:6px 12px;color:#4C9BE8;font-weight:bold;'>{seconds_to_hm(secs)}</td></tr>\"\n\nhtml = f\"\"\"\n<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<style>\n  body {{ font-family: Arial, sans-serif; background: #0f0f1a; color: #ffffff; margin: 0; padding: 0; }}\n  .container {{ max-width: 800px; margin: 0 auto; padding: 30px 20px; }}\n  .header {{ background: linear-gradient(135deg, #1a1a2e, #16213e); border-radius: 12px; padding: 30px; margin-bottom: 30px; text-align: center; border: 1px solid #4C9BE8; }}\n  .header h1 {{ margin: 0; font-size: 28px; color: #4C9BE8; }}\n  .header p {{ margin: 8px 0 0; color: #aaa; font-size: 14px; }}\n  .stats {{ width: 100%; margin-bottom: 30px; }}\n  .stat-box {{ display: inline-block; width: 30%; background: #1a1a2e; border-radius: 10px; padding: 20px; text-align: center; border: 1px solid #333; vertical-align: top; }}\n  .stat-box .value {{ font-size: 26px; font-weight: bold; color: #4C9BE8; }}\n  .stat-box .label {{ font-size: 12px; color: #888; margin-top: 5px; }}\n  .section {{ background: #1a1a2e; border-radius: 10px; padding: 20px; margin-bottom: 25px; border: 1px solid #333; }}\n  .section h2 {{ margin: 0 0 15px; font-size: 16px; color: #4C9BE8; border-bottom: 1px solid #333; padding-bottom: 10px; }}\n  .chart img {{ width: 100% !important; max-width: 100% !important; height: auto !important; border-radius: 8px; display: block; }}\n  table {{ width: 100%; border-collapse: collapse; }}\n  tr:nth-child(even) {{ background: #16213e; }}\n  .footer {{ text-align: center; color: #555; font-size: 11px; margin-top: 30px; }}\n</style>\n</head>\n<body>\n<div class=\"container\">\n\n  <div class=\"header\">\n    <h1>Dev Activity Report</h1>\n    <p>Weekly summary \u2014 last 7 days</p>\n  </div>\n\n  <div class=\"stats\">\n    <div class=\"stat-box\">\n      <div class=\"value\">{total_active}</div>\n      <div class=\"label\">Active Time</div>\n    </div>\n    <div class=\"stat-box\">\n      <div class=\"value\">{total_idle}</div>\n      <div class=\"label\">Idle Time</div>\n    </div>\n    <div class=\"stat-box\">\n      <div class=\"value\">{len(data['by_category'])}</div>\n      <div class=\"label\">Categories</div>\n    </div>\n  </div>\n\n  <div class=\"section\">\n    <h2>Time per Category</h2>\n    <table>{category_rows}</table>\n  </div>\n\n  <div class=\"section chart\">\n    <h2>Category Breakdown</h2>\n    <img width=\"700\" src=\"{data['chart_category']}\">\n  </div>\n\n  <div class=\"section chart\">\n    <h2>Daily Activity</h2>\n    <img width=\"700\" src=\"{data['chart_daily']}\">\n  </div>\n\n  <div class=\"section chart\">\n    <h2>Category per Day</h2>\n    <img width=\"700\" src=\"{data['chart_stacked']}\">\n  </div>\n\n  <div class=\"section chart\">\n    <h2>Top Apps</h2>\n    <img width=\"700\" src=\"{data['chart_top_apps']}\">\n  </div>\n\n  <div class=\"section chart\">\n    <h2>Active vs Idle</h2>\n    <img width=\"700\" src=\"{data['chart_idle']}\">\n  </div>\n\n  <div class=\"section chart\">\n    <h2>Productivity Heatmap</h2>\n    <img width=\"700\" src=\"{data['chart_heatmap']}\">\n  </div>\n\n  <div class=\"footer\">\n    Generated by Dev-Tracker \u00b7 Privacy-first activity monitoring\n  </div>\n\n</div>\n</body>\n</html>\n\"\"\"\n\nreturn [{'json': {'subject': 'Dev Activity \u2014 Weekly Report', 'html': html}}]"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        784,
        464
      ],
      "id": "fb4969d7-430e-46ea-bf65-cd14f6130dc7",
      "name": "Format to HTML"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "f1d6349f-ed13-4960-8809-2451b41d888b",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -48,
        224
      ],
      "id": "985a2bad-ca2f-42f4-8dce-73b682423e28",
      "name": "Data listener"
    },
    {
      "parameters": {
        "fieldToSplitOut": "body.sessions",
        "options": {}
      },
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        160,
        224
      ],
      "id": "3f778f55-f112-4edc-827a-df04937554c1",
      "name": "Split data to rows"
    },
    {
      "parameters": {
        "jsCode": "for (const item of $input.all()) {\n  item.json.start_time = item.json.start_time.replace(' ', 'T');\n  item.json.end_time = item.json.end_time.replace(' ', 'T');\n}\nreturn $input.all();"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        368,
        224
      ],
      "id": "d33a4a32-e9a6-4bd3-80da-ebffecc9ab12",
      "name": "Change date time"
    },
    {
      "parameters": {
        "dataTableId": {
          "__rl": true,
          "value": "61EXjt0nLCKmZ7ON",
          "mode": "list",
          "cachedResultName": "dev_activity_daemon",
          "cachedResultUrl": "/projects/57QGut0RrnCgx05p/datatables/61EXjt0nLCKmZ7ON"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "is_idle": "={{ $json.is_idle }}",
            "session_id": "={{ $json.id }}",
            "window_title": "={{ $json.window_title }}",
            "process_name": "={{ $json.process_name }}",
            "category": "={{ $json.category }}",
            "start_time": "={{ $json.start_time }}",
            "end_time": "={{ $json.end_time }}",
            "duration_seconds": "={{ $json.duration_seconds }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "session_id",
              "displayName": "session_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "window_title",
              "displayName": "window_title",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "process_name",
              "displayName": "process_name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "category",
              "displayName": "category",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "start_time",
              "displayName": "start_time",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "end_time",
              "displayName": "end_time",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "duration_seconds",
              "displayName": "duration_seconds",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "is_idle",
              "displayName": "is_idle",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "boolean",
              "readOnly": false,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        576,
        224
      ],
      "id": "5de661c9-17a8-4676-b33c-5502f906bcc0",
      "name": "Insert data rows to n8n table"
    },
    {
      "parameters": {
        "language": "pythonNative",
        "pythonCode": "from datetime import datetime, timezone\nimport pandas as pd\n\nrows = [item['json'] for item in _items]\ndf = pd.DataFrame(rows)\n\n# --- Parse and clean ---\ndf[\"start_time\"] = pd.to_datetime(df[\"start_time\"], utc=True)\ndf[\"end_time\"] = pd.to_datetime(df[\"end_time\"], utc=True)\ndf[\"duration_seconds\"] = pd.to_numeric(df[\"duration_seconds\"], errors=\"coerce\").fillna(0)\ndf[\"is_idle\"] = df[\"is_idle\"].astype(bool)\ndf[\"category\"] = df[\"category\"].fillna(\"Uncategorized\")\n\ndf_active = df[df[\"is_idle\"] == False].copy()\ndf_idle = df[df[\"is_idle\"] == True].copy()\n\ntotal_active = int(df_active[\"duration_seconds\"].sum())\ntotal_idle = int(df_idle[\"duration_seconds\"].sum())\n\n# --- 1. Time per category ---\nby_category = (\n    df_active.groupby(\"category\")[\"duration_seconds\"]\n    .sum()\n    .sort_values(ascending=False)\n    .astype(int)\n    .to_dict()\n)\n\n# --- 2. Top 10 apps + Others ---\napp_totals = (\n    df_active.groupby(\"window_title\")[\"duration_seconds\"]\n    .sum()\n    .sort_values(ascending=False)\n)\ntop_10 = app_totals.head(10).astype(int).to_dict()\nothers_sum = int(app_totals.iloc[10:].sum())\nif others_sum > 0:\n    top_10[\"Others\"] = others_sum\ntop_apps_pie = top_10\n\n# --- 3. Weekly breakdown ---\ndf_active[\"week\"] = df_active[\"start_time\"].dt.isocalendar().week.astype(int)\ndf_active[\"week_label\"] = df_active[\"start_time\"].dt.strftime(\"W%V %d.%m\")\n# Group by week number, use first date as label\nweek_labels = (\n    df_active.groupby(\"week\")[\"week_label\"]\n    .first()\n    .to_dict()\n)\nweekly_totals = (\n    df_active.groupby(\"week\")[\"duration_seconds\"]\n    .sum()\n    .astype(int)\n    .to_dict()\n)\nweekly_breakdown = {\n    week_labels[w]: round(weekly_totals[w] / 3600, 2)\n    for w in sorted(week_labels.keys())\n}\n\n# --- 4. Best day of week ---\nWEEKDAY_ORDER = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"]\ndf_active[\"weekday\"] = df_active[\"start_time\"].dt.strftime(\"%a\")\nbest_day = (\n    df_active.groupby(\"weekday\")[\"duration_seconds\"]\n    .sum()\n    .reindex(WEEKDAY_ORDER, fill_value=0)\n    .astype(int)\n    .to_dict()\n)\n\n# --- 5. Hour of day distribution ---\ndf_active[\"hour\"] = df_active[\"start_time\"].dt.hour\nhour_dist = (\n    df_active.groupby(\"hour\")[\"duration_seconds\"]\n    .sum()\n    .reindex(range(24), fill_value=0)\n    .astype(int)\n    .to_dict()\n)\nhour_dist = {str(k): v for k, v in hour_dist.items()}\n\n# --- 6. Daily calendar heatmap ---\ndf_active[\"day\"] = df_active[\"start_time\"].dt.strftime(\"%Y-%m-%d\")\ndaily_totals = (\n    df_active.groupby(\"day\")[\"duration_seconds\"]\n    .sum()\n    .astype(int)\n    .to_dict()\n)\n\nreturn [{\n    \"json\": {\n        \"total_active_seconds\": total_active,\n        \"total_idle_seconds\": total_idle,\n        \"by_category\": by_category,\n        \"top_apps_pie\": top_apps_pie,\n        \"weekly_breakdown\": weekly_breakdown,\n        \"best_day\": best_day,\n        \"hour_dist\": hour_dist,\n        \"daily_totals\": daily_totals,\n        \"weekday_order\": WEEKDAY_ORDER,\n    }\n}]"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        368,
        704
      ],
      "id": "46726cc2-06f6-4a6a-a9b0-5ff1e94a8029",
      "name": "Month analysis"
    },
    {
      "parameters": {
        "language": "pythonNative",
        "pythonCode": "import json\nimport urllib.request\nimport urllib.parse\n\ndata = _items[0]['json']\n\nCOLORS = [\n    '#4C9BE8', '#E87B4C', '#4CE87B', '#E84C9B',\n    '#9B4CE8', '#E8E84C', '#4CE8E8', '#E84C4C',\n    '#9BE84C', '#4C4CE8', '#888888'\n]\n\ndef make_short_url(config):\n    payload = json.dumps({\n        \"chart\": config,\n        \"width\": 600,\n        \"height\": 300,\n        \"backgroundColor\": \"#1a1a2e\"\n    }).encode('utf-8')\n    req = urllib.request.Request(\n        \"https://quickchart.io/chart/create\",\n        data=payload,\n        headers={\"Content-Type\": \"application/json\"},\n        method=\"POST\"\n    )\n    with urllib.request.urlopen(req, timeout=10) as resp:\n        result = json.loads(resp.read().decode())\n        return result[\"url\"]\n\n# --- Chart 1: Time per category (horizontal bar) ---\ncat_items = [(k, round(v / 3600, 2)) for k, v in data['by_category'].items() if v > 0]\ncats = [x[0] for x in cat_items]\nvals = [x[1] for x in cat_items]\n\nchart_category = make_short_url({\n    \"type\": \"horizontalBar\",\n    \"data\": {\n        \"labels\": cats,\n        \"datasets\": [{\n            \"label\": \"Hours\",\n            \"data\": vals,\n            \"backgroundColor\": COLORS[:len(cats)]\n        }]\n    },\n    \"options\": {\n        \"legend\": {\"labels\": {\"fontColor\": \"white\"}},\n        \"scales\": {\n            \"xAxes\": [{\"ticks\": {\"fontColor\": \"white\"}, \"gridLines\": {\"color\": \"#333\"}}],\n            \"yAxes\": [{\"ticks\": {\"fontColor\": \"white\"}, \"gridLines\": {\"color\": \"#333\"}}]\n        }\n    }\n})\n\n# --- Chart 2: Top apps pie ---\napp_labels = list(data['top_apps_pie'].keys())\napp_vals_seconds = list(data['top_apps_pie'].values())\napp_vals = [round(v / 60) for v in app_vals_seconds]  # minutes, for chart display\n\nshort_labels = [\n    (l[:30] + '...' if len(l) > 30 else l) + f\"  [{sec_to_hm(v)}]\"\n    for l, v in zip(app_labels, app_vals_seconds)  # legend uses original seconds\n]\n\nchart_top_apps = make_short_url({\n    \"type\": \"pie\",\n    \"data\": {\n        \"labels\": short_labels,\n        \"datasets\": [{\n            \"data\": app_vals,\n            \"backgroundColor\": COLORS[:len(app_vals)]\n        }]\n    },\n    \"options\": {\n        \"legend\": {\n            \"position\": \"right\",\n            \"labels\": {\"fontColor\": \"white\", \"fontSize\": 10}\n        }\n    }\n})\n\n# --- Chart 3: Active vs Idle doughnut ---\nactive_h = round(data['total_active_seconds'] / 3600, 2)\nidle_h = round(data['total_idle_seconds'] / 3600, 2)\nif active_h == 0 and idle_h == 0:\n    idle_h = 0.01\n\ntotal_h = active_h + idle_h\nactive_pct = round(100 * active_h / total_h, 1) if total_h > 0 else 0\nidle_pct = round(100 * idle_h / total_h, 1) if total_h > 0 else 0\n\nchart_idle = make_url({\n    \"type\": \"doughnut\",\n    \"data\": {\n        \"labels\": [f\"Active {active_h}h ({active_pct}%)\", f\"Idle {idle_h}h ({idle_pct}%)\"],\n        \"datasets\": [{\n            \"data\": [active_h, idle_h],\n            \"backgroundColor\": [\"#4C9BE8\", \"#E84C4C\"]\n        }]\n    },\n    \"options\": {\n        \"legend\": {\n            \"position\": \"bottom\",\n            \"labels\": {\"fontColor\": \"white\"}\n        }\n    }\n})\n\n# --- Chart 4: Weekly breakdown bar ---\nweek_labels = list(data['weekly_breakdown'].keys())\nweek_vals = list(data['weekly_breakdown'].values())\n\nchart_weekly = make_short_url({\n    \"type\": \"bar\",\n    \"data\": {\n        \"labels\": week_labels,\n        \"datasets\": [{\n            \"label\": \"Hours\",\n            \"data\": week_vals,\n            \"backgroundColor\": \"#4CE87B\"\n        }]\n    },\n    \"options\": {\n        \"legend\": {\"labels\": {\"fontColor\": \"white\"}},\n        \"scales\": {\n            \"xAxes\": [{\"ticks\": {\"fontColor\": \"white\"}, \"gridLines\": {\"color\": \"#333\"}}],\n            \"yAxes\": [{\"ticks\": {\"fontColor\": \"white\"}, \"gridLines\": {\"color\": \"#333\"}}]\n        }\n    }\n})\n\n# --- Chart 5: Best day of week ---\ndays = data['weekday_order']\nday_vals = [round(data['best_day'].get(d, 0) / 3600, 2) for d in days]\n\nchart_best_day = make_short_url({\n    \"type\": \"bar\",\n    \"data\": {\n        \"labels\": days,\n        \"datasets\": [{\n            \"label\": \"Hours\",\n            \"data\": day_vals,\n            \"backgroundColor\": COLORS[:len(days)]\n        }]\n    },\n    \"options\": {\n        \"legend\": {\"labels\": {\"fontColor\": \"white\"}},\n        \"scales\": {\n            \"xAxes\": [{\"ticks\": {\"fontColor\": \"white\"}, \"gridLines\": {\"color\": \"#333\"}}],\n            \"yAxes\": [{\"ticks\": {\"fontColor\": \"white\"}, \"gridLines\": {\"color\": \"#333\"}}]\n        }\n    }\n})\n\n# --- Chart 6: Hour of day distribution ---\nhours = [str(h) for h in range(24)]\nhour_vals = [round(data['hour_dist'].get(str(h), 0) / 3600, 2) for h in range(24)]\n\nchart_hour = make_short_url({\n    \"type\": \"bar\",\n    \"data\": {\n        \"labels\": hours,\n        \"datasets\": [{\n            \"label\": \"Hours\",\n            \"data\": hour_vals,\n            \"backgroundColor\": \"#9B4CE8\"\n        }]\n    },\n    \"options\": {\n        \"legend\": {\"labels\": {\"fontColor\": \"white\"}},\n        \"scales\": {\n            \"xAxes\": [{\"ticks\": {\"fontColor\": \"white\"}, \"gridLines\": {\"color\": \"#333\"}}],\n            \"yAxes\": [{\"ticks\": {\"fontColor\": \"white\"}, \"gridLines\": {\"color\": \"#333\"}}]\n        }\n    }\n})\n\n# --- Chart 7: Daily calendar heatmap (bubble) ---\ndaily_data = []\nsorted_days = sorted(data['daily_totals'].keys())\nfor i, day in enumerate(sorted_days):\n    secs = data['daily_totals'][day]\n    daily_data.append({\n        \"x\": i + 1,\n        \"y\": 0,\n        \"r\": min(20, max(3, int(secs / 60)))\n    })\n\nday_labels = [d[8:] + \".\" + d[5:7] for d in sorted_days]\n\nchart_calendar = make_short_url({\n    \"type\": \"bubble\",\n    \"data\": {\n        \"datasets\": [{\n            \"label\": \"Activity\",\n            \"data\": daily_data,\n            \"backgroundColor\": \"#4C9BE8\"\n        }]\n    },\n    \"options\": {\n        \"legend\": {\"labels\": {\"fontColor\": \"white\"}},\n        \"scales\": {\n            \"xAxes\": [{\n                \"ticks\": {\"fontColor\": \"white\", \"min\": 1, \"max\": len(sorted_days)},\n                \"gridLines\": {\"color\": \"#333\"},\n                \"scaleLabel\": {\"display\": True, \"labelString\": \"Day of month\", \"fontColor\": \"white\"}\n            }],\n            \"yAxes\": [{\n                \"ticks\": {\"fontColor\": \"white\", \"display\": False},\n                \"gridLines\": {\"color\": \"#333\"}\n            }]\n        }\n    }\n})\n\nreturn [{'json': {\n    'chart_category': chart_category,\n    'chart_top_apps': chart_top_apps,\n    'chart_idle': chart_idle,\n    'chart_weekly': chart_weekly,\n    'chart_best_day': chart_best_day,\n    'chart_hour': chart_hour,\n    'chart_calendar': chart_calendar,\n    'total_active_seconds': data['total_active_seconds'],\n    'total_idle_seconds': data['total_idle_seconds'],\n    'by_category': data['by_category'],\n    'weekday_order': data['weekday_order'],\n}}]"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        576,
        704
      ],
      "id": "dd35571d-3919-4202-b14f-c151622f86a8",
      "name": "Month graphs creator"
    },
    {
      "parameters": {
        "language": "pythonNative",
        "pythonCode": "data = _items[0]['json']\n\ndef seconds_to_hm(s):\n    h = int(s // 3600)\n    m = int((s % 3600) // 60)\n    return f\"{h}h {m}m\"\n\ntotal_active = seconds_to_hm(data['total_active_seconds'])\ntotal_idle = seconds_to_hm(data['total_idle_seconds'])\n\ncategory_rows = \"\"\nfor cat, secs in data['by_category'].items():\n    category_rows += f\"<tr><td style='padding:6px 12px;color:#ccc;'>{cat}</td><td style='padding:6px 12px;color:#4C9BE8;font-weight:bold;'>{seconds_to_hm(secs)}</td></tr>\"\n\nhtml = f\"\"\"\n<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<style>\n  body {{ font-family: Arial, sans-serif; background: #0f0f1a; color: #ffffff; margin: 0; padding: 0; }}\n  .container {{ max-width: 800px; margin: 0 auto; padding: 30px 20px; }}\n  .header {{ background: linear-gradient(135deg, #1a1a2e, #16213e); border-radius: 12px; padding: 30px; margin-bottom: 30px; text-align: center; border: 1px solid #4CE87B; }}\n  .header h1 {{ margin: 0; font-size: 28px; color: #4CE87B; }}\n  .header p {{ margin: 8px 0 0; color: #aaa; font-size: 14px; }}\n  .stats {{ width: 100%; margin-bottom: 30px; }}\n  .stat-box {{ display: inline-block; width: 30%; background: #1a1a2e; border-radius: 10px; padding: 20px; text-align: center; border: 1px solid #333; vertical-align: top; }}\n  .stat-box .value {{ font-size: 26px; font-weight: bold; color: #4CE87B; }}\n  .stat-box .label {{ font-size: 12px; color: #888; margin-top: 5px; }}\n  .section {{ background: #1a1a2e; border-radius: 10px; padding: 20px; margin-bottom: 25px; border: 1px solid #333; }}\n  .section h2 {{ margin: 0 0 15px; font-size: 16px; color: #4CE87B; border-bottom: 1px solid #333; padding-bottom: 10px; }}\n  .chart img {{ width: 100% !important; max-width: 100% !important; height: auto !important; border-radius: 8px; display: block; }}\n  table {{ width: 100%; border-collapse: collapse; }}\n  tr:nth-child(even) {{ background: #16213e; }}\n  .footer {{ text-align: center; color: #555; font-size: 11px; margin-top: 30px; }}\n</style>\n</head>\n<body>\n<div class=\"container\">\n\n  <div class=\"header\">\n    <h1>Dev Activity \u2014 Monthly Report</h1>\n    <p>Full month summary</p>\n  </div>\n\n  <div class=\"stats\">\n    <div class=\"stat-box\">\n      <div class=\"value\">{total_active}</div>\n      <div class=\"label\">Active Time</div>\n    </div>\n    <div class=\"stat-box\">\n      <div class=\"value\">{total_idle}</div>\n      <div class=\"label\">Idle Time</div>\n    </div>\n    <div class=\"stat-box\">\n      <div class=\"value\">{len(data['by_category'])}</div>\n      <div class=\"label\">Categories</div>\n    </div>\n  </div>\n\n  <div class=\"section\">\n    <h2>Time per Category</h2>\n    <table>{category_rows}</table>\n  </div>\n\n  <div class=\"section chart\">\n    <h2>Category Breakdown</h2>\n    <img width=\"700\" src=\"{data['chart_category']}\">\n  </div>\n\n  <div class=\"section chart\">\n    <h2>Weekly Breakdown</h2>\n    <img width=\"700\" src=\"{data['chart_weekly']}\">\n  </div>\n\n  <div class=\"section chart\">\n    <h2>Best Day of Week</h2>\n    <img width=\"700\" src=\"{data['chart_best_day']}\">\n  </div>\n\n  <div class=\"section chart\">\n    <h2>Peak Hours</h2>\n    <img width=\"700\" src=\"{data['chart_hour']}\">\n  </div>\n\n  <div class=\"section chart\">\n    <h2>Top Apps</h2>\n    <img width=\"700\" src=\"{data['chart_top_apps']}\">\n  </div>\n\n  <div class=\"section chart\">\n    <h2>Active vs Idle</h2>\n    <img width=\"700\" src=\"{data['chart_idle']}\">\n  </div>\n\n  <div class=\"section chart\">\n    <h2>Daily Activity Calendar</h2>\n    <img width=\"700\" src=\"{data['chart_calendar']}\">\n  </div>\n\n  <div class=\"footer\">\n    Generated by Dev-Tracker \u00b7 Privacy-first activity monitoring\n  </div>\n\n</div>\n</body>\n</html>\n\"\"\"\n\nreturn [{'json': {'subject': 'Dev Activity \u2014 Monthly Report', 'html': html}}]"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        784,
        704
      ],
      "id": "380f096d-fe36-4585-b97a-5b575629f759",
      "name": "Month to HTML"
    },
    {
      "parameters": {
        "fromEmail": "={{ $env.N8N_AGENT_EMAIL_FROM }}",
        "toEmail": "={{ $env.DEV_ACTIVITY_DAEMON_EMAIL_TO }}",
        "subject": "={{ $json.subject }}",
        "html": "={{ $json.html }}",
        "options": {}
      },
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        992,
        464
      ],
      "id": "f1ce02fc-3a1b-4fef-b283-489a3b8211ef",
      "name": "Send weekly report",
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "fromEmail": "={{ $env.N8N_AGENT_EMAIL_FROM }}",
        "toEmail": "={{ $env.DEV_ACTIVITY_DAEMON_EMAIL_TO }}",
        "subject": "={{ $json.subject }}",
        "html": "={{ $json.html }}",
        "options": {}
      },
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        992,
        704
      ],
      "id": "ed6876bf-f22a-4470-bff8-4ae897dc5cda",
      "name": "Send monthly report",
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "content": "Data collector",
        "height": 208,
        "width": 880
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -96,
        176
      ],
      "typeVersion": 1,
      "id": "6010e37a-e3ff-498b-822b-2d455cfea2ef",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "content": "Weekly report",
        "height": 208,
        "width": 1296
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -96,
        416
      ],
      "typeVersion": 1,
      "id": "ad6bb791-9b28-4afa-a9bb-33fb48d2586c",
      "name": "Sticky Note1"
    },
    {
      "parameters": {
        "content": "Monthly report",
        "height": 208,
        "width": 1296
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -96,
        656
      ],
      "typeVersion": 1,
      "id": "035d273b-e375-4ce6-bc22-46255f8e9642",
      "name": "Sticky Note2"
    }
  ],
  "connections": {
    "Weekly trigger": {
      "main": [
        [
          {
            "node": "Weekly getter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Weekly getter": {
      "main": [
        [
          {
            "node": "Week analysis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Week analysis": {
      "main": [
        [
          {
            "node": "Graphs creator",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Graphs creator": {
      "main": [
        [
          {
            "node": "Format to HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format to HTML": {
      "main": [
        [
          {
            "node": "Send weekly report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Monthly trigger": {
      "main": [
        [
          {
            "node": "Monthly getter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Data listener": {
      "main": [
        [
          {
            "node": "Split data to rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split data to rows": {
      "main": [
        [
          {
            "node": "Change date time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Change date time": {
      "main": [
        [
          {
            "node": "Insert data rows to n8n table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Monthly getter": {
      "main": [
        [
          {
            "node": "Month analysis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Month analysis": {
      "main": [
        [
          {
            "node": "Month graphs creator",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Month graphs creator": {
      "main": [
        [
          {
            "node": "Month to HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Month to HTML": {
      "main": [
        [
          {
            "node": "Send monthly report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false
  },
  "versionId": "f38f3889-1802-4c0d-960f-af69bf8208b0",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "zRnQTWANCXVr2WvG",
  "tags": [
    {
      "updatedAt": "2026-04-02T17:33:04.440Z",
      "createdAt": "2026-04-02T17:33:04.440Z",
      "id": "FQeOsmVLsgzoXLMD",
      "name": "data table"
    },
    {
      "updatedAt": "2026-03-12T07:14:31.968Z",
      "createdAt": "2026-03-12T07:14:31.968Z",
      "id": "LMI2wwmKQQRYls3c",
      "name": "mail"
    },
    {
      "updatedAt": "2026-04-02T17:32:50.941Z",
      "createdAt": "2026-04-02T17:32:50.941Z",
      "id": "m6g38sSfTF4up3Ao",
      "name": "webhook"
    },
    {
      "updatedAt": "2026-04-02T17:32:45.662Z",
      "createdAt": "2026-04-02T17:32:45.662Z",
      "id": "mq5yd5kT2zxvGvAc",
      "name": "python"
    }
  ]
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

dev_activity_reporter. Uses dataTable, emailSend. Scheduled trigger; 19 nodes.

Source: https://github.com/Jarkendar/dev_activity_deamon/blob/71b43e5a962246e1e25bbdfb9898ecde7dc5f953/n8n/dev_activity_reporter.json — original creator credit. Request a take-down →

More General workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

General

Perfect for content publishing with organic scheduling patterns, social media automation, API systems that need to avoid rate limiting, or any automation requiring randomised timing control across mul

n8n, Read Write File, Stop And Error +1
General

Complete backup solution that saves both workflows and credentials to local/server disk with optional FTP upload for off-site redundancy.

Read Write File, Email Send, Execute Command +3
General

This n8n workflow automates the secure transfer of files between FTP servers on a scheduled basis, providing enterprise-grade reliability with comprehensive error handling and dual notification system

Ftp, Email Send
General

This workflow automatically monitors government regulatory changes and provides comprehensive compliance tracking and executive alerts. Scheduled Monitoring - Runs daily at 9 AM to check for new regul

N8N Nodes Scrapegraphai, Email Send
General

⚠️ Important: This workflow uses the Autype community node and requires a self-hosted n8n instance.

Noco Db, N8N Nodes Autype, Email Send