AutomationFlowsEmail & Gmail › Chart

Chart

chart. Uses httpRequest, postgres, executeWorkflowTrigger, gmail. Event-driven trigger; 7 nodes.

Event trigger★★★★☆ complexity7 nodesHTTP RequestPostgresExecute Workflow TriggerGmail
Email & Gmail Trigger: Event Nodes: 7 Complexity: ★★★★☆ Added:

This workflow follows the Execute Workflow Trigger → Gmail recipe pattern — see all workflows that pair these two integrations.

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": "chart",
  "nodes": [
    {
      "parameters": {
        "url": "={{ $json.url }}",
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "file",
              "outputPropertyName": "=chart"
            }
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        -224,
        32
      ],
      "id": "fc979267-447b-43cf-a091-0af19b4345cf",
      "name": "HTTP Request",
      "alwaysOutputData": true,
      "retryOnFail": false
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH all_rows AS (\n  -- CASE 1\n  SELECT\n    'case1'::text AS series,\n    ($1::text || ' vs ' || $2::text) AS series_label,\n    cdm_json,\n    created_at\n  FROM cdm_messages\n  WHERE\n    (lower(trim(pair_a)) = lower(trim($1)) AND lower(trim(pair_b)) = lower(trim($2)))\n    OR\n    (lower(trim(pair_a)) = lower(trim($2)) AND lower(trim(pair_b)) = lower(trim($1)))\n\n  UNION ALL\n\n  -- CASE 2\n  SELECT\n    'case2'::text AS series,\n    ($3::text || ' vs ' || $4::text) AS series_label,\n    cdm_json,\n    created_at\n  FROM cdm_messages\n  WHERE\n    (lower(trim(pair_a)) = lower(trim($3)) AND lower(trim(pair_b)) = lower(trim($4)))\n    OR\n    (lower(trim(pair_a)) = lower(trim($4)) AND lower(trim(pair_b)) = lower(trim($3)))\n\n  UNION ALL\n\n  -- CASE 3\n  SELECT\n    'case3'::text AS series,\n    ($5::text || ' vs ' || $6::text) AS series_label,\n    cdm_json,\n    created_at\n  FROM cdm_messages\n  WHERE\n    (lower(trim(pair_a)) = lower(trim($5)) AND lower(trim(pair_b)) = lower(trim($6)))\n    OR\n    (lower(trim(pair_a)) = lower(trim($6)) AND lower(trim(pair_b)) = lower(trim($5)))\n\n  UNION ALL\n\n  -- CASE 4\n  SELECT\n    'case4'::text AS series,\n    ($7::text || ' vs ' || $8::text) AS series_label,\n    cdm_json,\n    created_at\n  FROM cdm_messages\n  WHERE\n    (lower(trim(pair_a)) = lower(trim($7)) AND lower(trim(pair_b)) = lower(trim($8)))\n    OR\n    (lower(trim(pair_a)) = lower(trim($8)) AND lower(trim(pair_b)) = lower(trim($7)))\n),\n\nbase AS (\n  SELECT\n    series,\n    series_label,\n    cdm_json,\n    COALESCE(\n      NULLIF(cdm_json::jsonb->>'creation_date','')::timestamptz,\n      created_at\n    ) AS creation_ts\n  FROM all_rows\n),\n\nranked0 AS (\n  SELECT\n    series,\n    series_label,\n    cdm_json,\n    creation_ts,\n    row_number() OVER (\n      PARTITION BY series\n      ORDER BY creation_ts ASC\n    ) AS rn\n  FROM base\n),\n\nranked AS (\n  SELECT\n    series,\n    series_label,\n    rn AS cdm_n,\n    ('CDM #' || rn) AS cdm_label,\n    creation_ts AS creation_date,\n\n    -- Miss distance (m)\n    NULLIF(cdm_json::jsonb #>> '{relative_metadata_data,miss_distance,value}', '')::float8 AS miss_m,\n\n    -- Collision probability (Pc)\n    NULLIF(cdm_json::jsonb #>> '{relative_metadata_data,collision_probability}', '')::float8 AS pc,\n\n    -- Covariance diag terms (m^2) from each object (RTN)\n    NULLIF(cdm_json::jsonb #>> '{objects,object1,covariance_rtn,pos_pos,cr_r}', '')::float8 AS c1_rr,\n    NULLIF(cdm_json::jsonb #>> '{objects,object1,covariance_rtn,pos_pos,ct_t}', '')::float8 AS c1_tt,\n    NULLIF(cdm_json::jsonb #>> '{objects,object1,covariance_rtn,pos_pos,cn_n}', '')::float8 AS c1_nn,\n\n    NULLIF(cdm_json::jsonb #>> '{objects,object2,covariance_rtn,pos_pos,cr_r}', '')::float8 AS c2_rr,\n    NULLIF(cdm_json::jsonb #>> '{objects,object2,covariance_rtn,pos_pos,ct_t}', '')::float8 AS c2_tt,\n    NULLIF(cdm_json::jsonb #>> '{objects,object2,covariance_rtn,pos_pos,cn_n}', '')::float8 AS c2_nn\n  FROM ranked0\n)\n\nSELECT *\nFROM ranked\nORDER BY series, creation_date;",
        "options": {
          "queryReplacement": "=$1 = {{ $json.pair_a }}\n$2 = {{ $json.pair_b }}\n$3 = {{ $json['pair_a(other)'] }}\n$4 = {{ $json['pair_b(other)'] }}\n$5 = {{ $json['pair_a(other1)'] }}\n$6 = {{ $json['pair_b(other1)'] }}\n$7 ={{ $json['pair_a(other2)'] }}\n$8 ={{ $json['pair_b(other2)'] }}\n"
        }
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -1008,
        144
      ],
      "id": "3aa78a32-1fd7-4ee5-8498-514a038614fd",
      "name": "Execute a SQL query2",
      "executeOnce": true,
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const rows = $input.all().map(i => i.json);\n\n// --- \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 ---\nconst W = 1100;\nconst H = 900;\n\n// \u0435\u0441\u043b\u0438 true \u2014 \u0443\u043f\u0430\u0434\u0451\u0442 \u043e\u0448\u0438\u0431\u043a\u043e\u0439, \u0435\u0441\u043b\u0438 \u043d\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0445\nconst THROW_IF_EMPTY = true;\n\n// --- helpers ---\nfunction colorForIndex(i) {\n  const hue = (i * 47) % 360;                 // \u0434\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e \u0440\u0430\u0437\u043d\u044b\u0435 \u0446\u0432\u0435\u0442\u0430\n  return `hsl(${hue}, 70%, 50%)`;\n}\n\nfunction sortByN(arr) { arr.sort((a, b) => a.n - b.n); }\n\nfunction toAlignedData(points, maxN) {\n  const arr = Array(maxN).fill(null);\n  for (const p of points) {\n    const idx = p.n - 1;\n    if (idx >= 0 && idx < maxN) arr[idx] = p.y;\n  }\n  return arr;\n}\n\nfunction makeChart(titleY, datasets, yOptions = {}) {\n  return {\n    type: 'line',\n    data: { labels, datasets },\n    options: {\n      scales: {\n        x: { title: { display: true, text: 'CDM (old \u2192 new)' } },\n        y: {\n          title: { display: true, text: titleY },\n          ticks: { maxTicksLimit: 14 },\n          ...yOptions\n        }\n      },\n      plugins: {\n        legend: { display: true }\n      }\n    }\n  };\n}\n\nfunction makeUrl(chartConfig) {\n  return `https://quickchart.io/chart?width=${W}&height=${H}&format=png&c=` +\n    encodeURIComponent(JSON.stringify(chartConfig));\n}\n\n// --- group by pair_key ---\nconst byPair = new Map();\n\nfor (const r of rows) {\n  const pair = String(r.pair_key ?? '').trim();\n  if (!pair) continue;\n\n  const n = Number(r.cdm_n);\n  if (!Number.isFinite(n) || n <= 0) continue;\n\n  if (!byPair.has(pair)) {\n    byPair.set(pair, { miss: [], pc: [], sig: [] });\n  }\n  const g = byPair.get(pair);\n\n  // miss\n  const miss = Number(r.miss_m);\n  if (Number.isFinite(miss)) g.miss.push({ n, y: miss });\n\n  // pc (\u0442\u043e\u043b\u044c\u043a\u043e >0)\n  const pc = Number(r.pc);\n  if (Number.isFinite(pc) && pc > 0) g.pc.push({ n, y: pc });\n\n  // covariance overall 1\u03c3 (m): sqrt(rr+tt+nn), \u0433\u0434\u0435 rr/tt/nn \u2014 \u0441\u0443\u043c\u043c\u044b \u0432\u0430\u0440\u0438\u0430\u043d\u0441\u043e\u0432 \u0434\u0432\u0443\u0445 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432\n  const rr = Number(r.c1_rr) + Number(r.c2_rr);\n  const tt = Number(r.c1_tt) + Number(r.c2_tt);\n  const nn = Number(r.c1_nn) + Number(r.c2_nn);\n\n  if (Number.isFinite(rr) && Number.isFinite(tt) && Number.isFinite(nn)) {\n    const varSum = Math.max(0, rr) + Math.max(0, tt) + Math.max(0, nn);\n    const sigOverall = Math.sqrt(varSum);\n    if (Number.isFinite(sigOverall)) g.sig.push({ n, y: sigOverall });\n  }\n}\n\nconst pairs = Array.from(byPair.keys());\nif (!pairs.length) {\n  if (THROW_IF_EMPTY) throw new Error('\u041d\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0445: \u043d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e pair_key \u0441 \u0432\u0430\u043b\u0438\u0434\u043d\u044b\u043c cdm_n.');\n  return [];\n}\n\n// --- sort + find maxN ---\nlet maxN = 0;\nfor (const pair of pairs) {\n  const g = byPair.get(pair);\n  sortByN(g.miss); sortByN(g.pc); sortByN(g.sig);\n\n  for (const arr of [g.miss, g.pc, g.sig]) {\n    if (arr.length) maxN = Math.max(maxN, arr[arr.length - 1].n);\n  }\n}\n\nif (!maxN) {\n  if (THROW_IF_EMPTY) throw new Error('\u041d\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u043b\u044f \u0433\u0440\u0430\u0444\u0438\u043a\u043e\u0432 (\u0432\u0441\u0435 \u0441\u0435\u0440\u0438\u0438 \u043f\u0443\u0441\u0442\u044b\u0435).');\n  return [];\n}\n\nconst labels = Array.from({ length: maxN }, (_, i) => `CDM #${i + 1}`);\n\n// --- build datasets per metric ---\nfunction buildDatasets(metric) {\n  const out = [];\n  pairs.forEach((pair, idx) => {\n    const g = byPair.get(pair);\n    const data = toAlignedData(g[metric], maxN);\n    const c = colorForIndex(idx);\n    out.push({\n      label: `pair_key=${pair}`,\n      data,\n      fill: false,\n      spanGaps: true,\n      borderColor: c,\n      backgroundColor: c,\n      pointRadius: 4,\n      pointHoverRadius: 6,\n      borderWidth: 2,\n      tension: 0\n    });\n  });\n  return out;\n}\n\n// 1) Miss\nconst missChart = makeChart(\n  'Miss distance (m)',\n  buildDatasets('miss')\n);\n\n// 2) Pc (\u043b\u0438\u043d\u0435\u0439\u043d\u0430\u044f; \u0435\u0441\u043b\u0438 \u0445\u043e\u0447\u0435\u0448\u044c \u043b\u043e\u0433-\u043e\u0441\u044c \u2014 \u0441\u043a\u0430\u0436\u0438)\nconst pcChart = makeChart(\n  'Collision Probability (Pc)',\n  buildDatasets('pc')\n);\n\n// 3) Cov overall 1\u03c3\nconst sigChart = makeChart(\n  'Overall 1\u03c3 (m) from RTN covariance',\n  buildDatasets('sig')\n);\n\nreturn [\n  { json: { kind: 'miss', pair_keys: pairs, url: makeUrl(missChart) } },\n  { json: { kind: 'pc',   pair_keys: pairs, url: makeUrl(pcChart) } },\n  { json: { kind: 'cov',  pair_keys: pairs, url: makeUrl(sigChart) } },\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -480,
        112
      ],
      "id": "7f023fcb-03bc-4073-acb6-ce04b351b380",
      "name": "Code in JavaScript"
    },
    {
      "parameters": {
        "workflowInputs": {
          "values": [
            {
              "name": "pair_a"
            },
            {
              "name": "pair_b"
            },
            {
              "name": "pair_a(other)"
            },
            {
              "name": "pair_b(other)"
            },
            {
              "name": "\u043e\u0431\u044c\u044f\u0441\u043d\u0435\u043d\u0438\u0435 "
            },
            {
              "name": "pair_a(other1)"
            },
            {
              "name": "pair_b(other1)"
            },
            {
              "name": "pair_a(other2)"
            },
            {
              "name": "pair_b(other2)"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        -1312,
        112
      ],
      "id": "a02ccf0d-2b76-4bea-bb13-6284dd6b662d",
      "name": "When Executed by Another Workflow"
    },
    {
      "parameters": {
        "sendTo": "garfieldgg228@gmail.com",
        "subject": "\u0413\u0440\u0430\u0444\u0438\u043a",
        "message": "=there is charts",
        "options": {
          "attachmentsUi": {
            "attachmentsBinary": [
              {
                "property": "chart_1"
              },
              {
                "property": "chart_1"
              },
              {
                "property": "chart_2"
              },
              {
                "property": "chart_3"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        192,
        0
      ],
      "id": "6d805399-897d-4f8c-b769-c16e250a1fd0",
      "name": "Send a message",
      "executeOnce": true,
      "alwaysOutputData": true,
      "retryOnFail": false,
      "waitBetweenTries": 5000,
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\n\nconst out = {\n  json: { requested: items.length, included: 0, details: [] },\n  binary: {},\n};\n\nlet k = 1;\n\nfor (let i = 0; i < items.length; i++) {\n  const bin = items[i].binary || {};\n  const keys = Object.keys(bin);\n\n  if (keys.length === 0) {\n    out.json.details.push({ item: i, ok: false, reason: \"no binary\" });\n    continue;\n  }\n\n  // \u043f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442: chart -> data -> \u043f\u0435\u0440\u0432\u044b\u0439 \u043f\u043e\u043f\u0430\u0432\u0448\u0438\u0439\u0441\u044f \u043a\u043b\u044e\u0447\n  const srcKey = bin.chart ? \"chart\" : (bin.data ? \"data\" : keys[0]);\n  const b = bin[srcKey];\n\n  const dstKey = `chart_${k}`; // \u043a\u043b\u044e\u0447 \u0411\u0415\u0417 .png\n  out.binary[dstKey] = {\n    ...b,\n    fileName: `chart_${k}.png`,           // \u0438\u043c\u044f \u0444\u0430\u0439\u043b\u0430 \u0443\u0436\u0435 \u0441 .png\n    mimeType: b.mimeType || \"image/png\",\n  };\n\n  out.json.details.push({ item: i, ok: true, from: srcKey, to: dstKey });\n  out.json.included++;\n  k++;\n}\n\nreturn [out];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        0,
        0
      ],
      "id": "529dac35-4f17-48f8-9323-8cd182199c7b",
      "name": "Code in JavaScript1"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH pairs(ord, series, pair_a, pair_b) AS (\n  VALUES\n    (1, 'case1'::text, $1::text, $2::text),\n    (2, 'case2'::text, $3::text, $4::text),\n    (3, 'case3'::text, $5::text, $6::text),\n    (4, 'case4'::text, $7::text, $8::text)\n),\ncanon_pairs AS (\n  SELECT\n    series,\n    least(upper(trim(pair_a)), upper(trim(pair_b)))    AS a_can,\n    greatest(upper(trim(pair_a)), upper(trim(pair_b))) AS b_can\n  FROM pairs\n  WHERE pair_a IS NOT NULL AND pair_b IS NOT NULL\n    AND trim(pair_a) <> '' AND trim(pair_b) <> ''\n),\ndiff_keys AS (\n  SELECT\n    least(upper(trim(pair_a)), upper(trim(pair_b)))    AS a_can,\n    greatest(upper(trim(pair_a)), upper(trim(pair_b))) AS b_can,\n    MIN(pair_key) AS pair_key              -- pair_key \u043d\u0435 \u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f -> \u0445\u0432\u0430\u0442\u0430\u0435\u0442 MIN\n  FROM difference\n  WHERE pair_a IS NOT NULL AND pair_b IS NOT NULL\n    AND trim(pair_a) <> '' AND trim(pair_b) <> ''\n    AND pair_key IS NOT NULL AND trim(pair_key) <> ''\n  GROUP BY 1,2\n),\npair_map AS (\n  SELECT\n    cp.series,\n    dk.pair_key\n  FROM canon_pairs cp\n  LEFT JOIN diff_keys dk\n    ON dk.a_can = cp.a_can AND dk.b_can = cp.b_can\n),\ninput_rows AS (\n  SELECT value AS j\n  FROM jsonb_array_elements($9::jsonb)\n)\nSELECT\n  j->>'series'       AS series,\n  j->>'series_label' AS series_label,\n  j->>'cdm_n'        AS cdm_n,\n  j->>'cdm_label'    AS cdm_label,\n  j->>'creation_date' AS creation_date,\n\n  NULLIF(j->>'miss_m','')::float8 AS miss_m,\n  NULLIF(j->>'pc','')::float8     AS pc,\n\n  NULLIF(j->>'c1_rr','')::float8 AS c1_rr,\n  NULLIF(j->>'c1_tt','')::float8 AS c1_tt,\n  NULLIF(j->>'c1_nn','')::float8 AS c1_nn,\n\n  NULLIF(j->>'c2_rr','')::float8 AS c2_rr,\n  NULLIF(j->>'c2_tt','')::float8 AS c2_tt,\n  NULLIF(j->>'c2_nn','')::float8 AS c2_nn,\n\n  pm.pair_key        AS pair_key\n\nFROM input_rows\nLEFT JOIN pair_map pm\n  ON pm.series = (j->>'series')\nORDER BY series, NULLIF(j->>'cdm_n','')::int;",
        "options": {
          "queryReplacement": "=$1 = {{ $('When Executed by Another Workflow').item.json.pair_a }}\n$2 = {{ $('When Executed by Another Workflow').item.json.pair_b }}\n$3 = {{ $('When Executed by Another Workflow').item.json['pair_a(other)'] }}\n$4 = {{ $('When Executed by Another Workflow').item.json['pair_b(other)'] }}\n$5 = {{ $('When Executed by Another Workflow').item.json['pair_a(other1)'] }}\n$6 ={{ $('When Executed by Another Workflow').item.json['pair_b(other1)'] }}\n$7 = {{ $('When Executed by Another Workflow').item.json['pair_a(other2)'] }}\n$8 ={{ $('When Executed by Another Workflow').item.json['pair_b(other2)'] }}\n$9 = {{ JSON.stringify($input.all().map(i => i.json)) }}"
        }
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -784,
        144
      ],
      "id": "be6a35d1-aa83-40c5-b181-b888d038d153",
      "name": "Execute a SQL query",
      "executeOnce": true,
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "HTTP Request": {
      "main": [
        [
          {
            "node": "Code in JavaScript1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute a SQL query2": {
      "main": [
        [
          {
            "node": "Execute a SQL query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript": {
      "main": [
        [
          {
            "node": "HTTP Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Executed by Another Workflow": {
      "main": [
        [
          {
            "node": "Execute a SQL query2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript1": {
      "main": [
        [
          {
            "node": "Send a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute a SQL query": {
      "main": [
        [
          {
            "node": "Code in JavaScript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false
  },
  "versionId": "5bea5b2b-7c41-449d-a0bf-9a3cae666486",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "pFWavyZYNdprhQKz",
  "tags": []
}

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

chart. Uses httpRequest, postgres, executeWorkflowTrigger, gmail. Event-driven trigger; 7 nodes.

Source: https://github.com/garfieldgg228-g423/Aktau-esa5-DEMO/blob/f235e9619920970a642f882710ed20992602ea80/workflows/chart.json — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

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

Email & Gmail

Splitout Code. Uses manualTrigger, httpRequest, stickyNote, splitOut. Event-driven trigger; 46 nodes.

HTTP Request, Execute Workflow Trigger, Gmail +1
Email & Gmail

Automate CSV imports into HubSpot without the mess. Powered by n8n. Supercharged by Pollup AI.

HTTP Request, Execute Workflow Trigger, Gmail +1
Email & Gmail

Echo Brand Voice Analysis (Processor) - TASK-074 Dec 10 Fix. Uses formTrigger, httpRequest, executeWorkflowTrigger, moveBinaryData. Event-driven trigger; 40 nodes.

Form Trigger, HTTP Request, Execute Workflow Trigger +2
Email & Gmail

Loxone MCP Client - Integration Hub. Uses start, googleCalendar, slack, mcp. Event-driven trigger; 20 nodes.

Start, Google Calendar, Slack +4
Email & Gmail

org-ai Dept Sales. Uses executeWorkflowTrigger, httpRequest, gmail. Event-driven trigger; 9 nodes.

Execute Workflow Trigger, HTTP Request, Gmail