Keep track of new stock market IPOs

I’ve created my first workflow that allows a person to watch new stock market IPOs for certain keywords and track companies as they move through the IPO process.

Goal: Make it easy to know the approximate date when a company will be tradeable on public markets

This workflow:

  1. Pulls data from the NASDAQ API
  2. Allows you to filter the data based on certain keywords (e.g. company names, ticker symbols, etc.)
  3. Tracks specific IPOs as they move through the process (i.e. filed -> upcoming -> priced)
  4. Sends an email notification if the status changes (e.g. new IPO appears or IPO status changes)

Before I share this on n8n.io/workflows, I would appreciate it if the experts here could review my work. I think I caught all the bugs, but would love some feedback/suggestions :slight_smile: , especially around:

  • Areas where I can do things more simply
  • Better ways to write/package parts of the workflow to make it more “reusable” for users of n8n.io/workflows
  • Any unintended bugs that I missed (they probably exist :smiley: )

Here is the workflow:

{
  "name": "IPO Upcoming Check",
  "nodes": [
    {
      "parameters": {},
      "name": "Start",
      "type": "n8n-nodes-base.start",
      "typeVersion": 1,
      "position": [
        -1590,
        610
      ]
    },
    {
      "parameters": {
        "functionCode": "return items[0].json.map(item => { \n  return {\n    json: item,\n  }\n})\n"
      },
      "name": "Create JSON-items",
      "type": "n8n-nodes-base.function",
      "position": [
        -877,
        607
      ],
      "typeVersion": 1
    },
    {
      "parameters": {
        "url": "\thttps://api.nasdaq.com/api/ipo/calendar",
        "options": {}
      },
      "name": "Get IPO Data",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        -1257,
        607
      ]
    },
    {
      "parameters": {
        "options": {}
      },
      "name": "Move Binary Data1",
      "type": "n8n-nodes-base.moveBinaryData",
      "typeVersion": 1,
      "position": [
        190,
        330
      ]
    },
    {
      "parameters": {
        "fileName": "=/home/node/.n8n/binary-data/ipo-checker_{{$node[\"If false reference\"].json[\"proposedTickerSymbol\"]}}.json"
      },
      "name": "Write symbol's file",
      "type": "n8n-nodes-base.writeBinaryFile",
      "typeVersion": 1,
      "position": [
        390,
        500
      ]
    },
    {
      "parameters": {
        "filePath": "=/home/node/.n8n/binary-data/ipo-checker_{{$node[\"Symbols where file exists\"].json[\"proposedTickerSymbol\"]}}.json"
      },
      "name": "Read symbol's file",
      "type": "n8n-nodes-base.readBinaryFile",
      "typeVersion": 1,
      "position": [
        30,
        330
      ]
    },
    {
      "parameters": {
        "resource": "message",
        "subject": "=IPO Upcoming Check: {{$json[\"proposedTickerSymbol\"]}} is upcoming.",
        "message": "= {{$json[\"proposedTickerSymbol\"]}} ({{$json[\"companyName\"]}}) has a pricing upcoming. The expected price date is {{$json[\"expectedPriceDate\"]}}. The proposed share price is {{$json[\"proposedSharePrice\"]}}.\n\nFind out more at https://www.nasdaq.com/market-activity/ipos.",
        "toList": [
          ""
        ],
        "additionalFields": {
          "bccList": []
        }
      },
      "name": "Send `Upcoming` mail",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 1,
      "position": [
        910,
        800
      ],
      "credentials": {
        "gmailOAuth2": ""
      }
    },
    {
      "parameters": {
        "resource": "message",
        "subject": "=IPO Upcoming Check: {{$json[\"proposedTickerSymbol\"]}} has filed.",
        "message": "= {{$json[\"proposedTickerSymbol\"]}} ({{$json[\"companyName\"]}}) filed for IPO on {{$json[\"filedDate\"]}}. Pricing to come. Check it out at https://www.nasdaq.com/market-activity/ipos.",
        "toList": [
          ""
        ],
        "additionalFields": {
          "bccList": []
        }
      },
      "name": "Send `Filed` mail",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 1,
      "position": [
        910,
        500
      ],
      "credentials": {
        "gmailOAuth2": ""
      }
    },
    {
      "parameters": {
        "resource": "message",
        "subject": "=IPO Upcoming Check: {{$json[\"proposedTickerSymbol\"]}} has priced.",
        "message": "= {{$json[\"proposedTickerSymbol\"]}} ({{$json[\"companyName\"]}}) is priced as of {{$json[\"pricedDate\"]}}. The proposed share price is {{$json[\"proposedSharePrice\"]}}. They are offering {{$json[\"sharesOffered\"]}} shares.\n\nFind out more at https://www.nasdaq.com/market-activity/ipos.",
        "toList": [
          ""
        ],
        "additionalFields": {
          "bccList": []
        }
      },
      "name": "Send `Priced` mail",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 1,
      "position": [
        910,
        650
      ],
      "credentials": {
        "gmailOAuth2": ""
      }
    },
    {
      "parameters": {
        "options": {}
      },
      "name": "Move Binary Data2",
      "type": "n8n-nodes-base.moveBinaryData",
      "typeVersion": 1,
      "position": [
        480,
        740
      ]
    },
    {
      "parameters": {},
      "name": "If false reference",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        -110,
        520
      ],
      "notesInFlow": true,
      "notes": "If ipoStatus is unchanged = `false`, use node as reference to only pass on `false` data (to prevent passing on `true` data into the write binary)"
    },
    {
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{$node[\"Check if file exists for symbol\"].json[\"stdout\"]}}",
              "operation": "contains",
              "value2": "exists"
            }
          ]
        }
      },
      "name": "If file exists for symbol",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        -337,
        497
      ]
    },
    {
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{$json[\"proposedTickerSymbol\"]}}",
              "operation": "contains",
              "value2": "FRON"
            },
            {
              "value1": "={{$json[\"proposedTickerSymbol\"]}}",
              "operation": "regex",
              "value2": "ARKX"
            },
            {
              "value1": "={{$json[\"companyName\"]}}",
              "operation": "regex",
              "value2": "/pathf/i"
            }
          ]
        },
        "combineOperation": "any"
      },
      "name": "Filter symbols",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        -710,
        600
      ],
      "notesInFlow": true,
      "notes": "Filter symbols by either `proposedTickerSymbol` or `companyName`"
    },
    {
      "parameters": {
        "functionCode": "let longTickerList = []\nlet filed = items[0].json.data.filed.rows;\nlet priced = items[0].json.data.priced.rows;\nlet upcoming = items[0].json.data.upcoming.upcomingTable.rows;\nlet combined = filed.concat(priced,upcoming);\n\n// denote status of IPO as new key/value pair\nfor(let i in combined){\n   if ('filedDate' in combined[i] === true) {\n       combined[i].ipoStatus = 'filed'\n   }\n   if ('pricedDate' in combined[i] === true) {\n       combined[i].ipoStatus = 'priced'\n   }\n   if ('expectedPriceDate' in combined[i] === true) {\n       combined[i].ipoStatus = 'upcoming'\n       //console.log(combined[i]);\n   }\n}\n\nlongTickerList.push({json:combined})\n\nreturn longTickerList;"
      },
      "name": "Combine all arrays and add `ipoStatus` key with value",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        -1057,
        607
      ]
    },
    {
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{$json[\"ipoStatus\"]}}",
              "value2": "={{$node[\"Symbols where file exists\"].json[\"ipoStatus\"]}}"
            }
          ]
        }
      },
      "name": "IF: ipoStatus is unchanged",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        380,
        330
      ]
    },
    {
      "parameters": {
        "filePath": "={{$json[\"fileName\"]}}"
      },
      "name": "Read symbol's file - 1",
      "type": "n8n-nodes-base.readBinaryFile",
      "typeVersion": 1,
      "position": [
        320,
        740
      ]
    },
    {
      "parameters": {
        "mode": "jsonToBinary",
        "options": {}
      },
      "name": "Move Binary Data0",
      "type": "n8n-nodes-base.moveBinaryData",
      "typeVersion": 1,
      "position": [
        240,
        500
      ]
    },
    {
      "parameters": {
        "dataType": "string",
        "value1": "={{$json[\"ipoStatus\"]}}",
        "rules": {
          "rules": [
            {
              "value2": "filed"
            },
            {
              "value2": "priced",
              "output": 1
            },
            {
              "value2": "upcoming",
              "output": 2
            }
          ]
        }
      },
      "name": "Route symbols by ipoStatus",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 1,
      "position": [
        690,
        740
      ]
    },
    {
      "parameters": {},
      "name": "No ipoStatus change",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        530,
        240
      ]
    },
    {
      "parameters": {
        "functionCode": "//for any symbols that have changed, return the API-response data\n//to write to file (instead of the data that was recently read\n//from file)\nlet matchedSymbols = []\nlet filterSymbols = $items(\"Filter symbols\");\nlet changedSymbols = $items(\"If false reference\");\n\n//iterate through changed symbols\nfor (var i = 0; i < changedSymbols.length; i++) {\n  let includesSymbol = false;\n  \t//for current changed symbol, iterate through filtered symbols\n\tfor (var j = 0; j < filterSymbols.length; j++) {\n\t\t//if proposedTickerSymbol of current filtered symbol matches current changed symbol\n    \tincludesSymbol = filterSymbols[j].json.proposedTickerSymbol.includes(changedSymbols[i].json.proposedTickerSymbol);\n    \t//console.log(filterSymbols[i].json.proposedTickerSymbol);\n    \t//console.log(includesSymbol + ' ' + changedSymbols[i].json.proposedTickerSymbol);\n  \n\t\t//if arrays include same symbol, add API-response array to new array\n\t\tif (includesSymbol === true) {\n\t\t\t//console.log('TRUE');\n\t\t\tmatchedSymbols.push(filterSymbols[j])\n\t\t\t//console.log(matchedSymbols);\n\t\t}\n\t}\n}\n\nreturn matchedSymbols;\n"
      },
      "name": "Return API-response data for changed symbols",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        70,
        500
      ]
    },
    {
      "parameters": {
        "functionCode": "//pass on only symbols for this branch\n\nlet matchedSymbols = []\n//use $items() instead of naming the node because this will get me\n//only the specific items sent to this node(instead of always\n//returning only the true items)\nlet fileExists = $items();\nlet filterSymbols = $items(\"Filter symbols\");\n\n//iterate through symbol files that exist\nfor (var i = 0; i < fileExists.length; i++) {\n  //get the symbol from file exists output\n  let fileExistsSymbol = fileExists[i].json.stdout.match(/[A-Z]+/);\n  //console.log(fileExistsSymbol);\n  for (var j = 0; j < filterSymbols.length; j++) {\n\t\t//match the symbol from file exists output to  the symbol from filter symbols output\n\t\tif (fileExistsSymbol == filterSymbols[j].json.proposedTickerSymbol) {\n\t\t\t//console.log(\"Symbols match! \" + fileExistsSymbol + \" \" + filterSymbols[j].json.proposedTickerSymbol);\n\t\t\tmatchedSymbols.push(filterSymbols[j]);\n\t\t}\n\t\telse {\n\t\t\t//console.log(\"Symbols don't match! \" + fileExistsSymbol + \" \" + filterSymbols[j].json.proposedTickerSymbol);\n\t\t}\n\t}\n}\n\nreturn matchedSymbols;\n"
      },
      "name": "Symbols where file exists",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        -140,
        330
      ],
      "notesInFlow": true,
      "notes": "This is needed due to issue highlighted here: https://community.n8n.io/t/data-from-earlier-node-coming-as-an-object-that-is-not-convertible-to-binary/4417/2"
    },
    {
      "parameters": {
        "functionCode": "//pass on only symbols for this branch\n\nlet matchedSymbols = []\n//use $items() instead of naming the node because this will get me\n//only the specific items sent to this node(instead of always\n//returning only the true items)\nlet fileExists = $items();\nlet filterSymbols = $items(\"Filter symbols\");\n\n//iterate through symbol files that exist\nfor (var i = 0; i < fileExists.length; i++) {\n  //get the symbol from file exists output\n  let fileExistsSymbol = fileExists[i].json.stdout.match(/[A-Z]+/);\n  //console.log(fileExistsSymbol);\n  for (var j = 0; j < filterSymbols.length; j++) {\n\t\t//match the symbol from file exists output to  the symbol from filter symbols output\n\t\tif (fileExistsSymbol == filterSymbols[j].json.proposedTickerSymbol) {\n\t\t\t//console.log(\"Symbols match! \" + fileExistsSymbol + \" \" + filterSymbols[j].json.proposedTickerSymbol);\n\t\t\tmatchedSymbols.push(filterSymbols[j]);\n\t\t}\n\t\telse {\n\t\t\t//console.log(\"Symbols don't match! \" + fileExistsSymbol + \" \" + filterSymbols[j].json.proposedTickerSymbol);\n\t\t}\n\t}\n}\n\nreturn matchedSymbols;\n"
      },
      "name": "Symbols where file does not exist",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        -160,
        740
      ],
      "notesInFlow": true,
      "notes": "This is needed due to issue highlighted here: https://community.n8n.io/t/data-from-earlier-node-coming-as-an-object-that-is-not-convertible-to-binary/4417/2"
    },
    {
      "parameters": {
        "mode": "jsonToBinary",
        "options": {}
      },
      "name": "Move Binary Data",
      "type": "n8n-nodes-base.moveBinaryData",
      "typeVersion": 1,
      "position": [
        10,
        740
      ]
    },
    {
      "parameters": {
        "fileName": "=/home/node/.n8n/binary-data/ipo-checker_{{$node[\"Symbols where file does not exist\"].json[\"proposedTickerSymbol\"]}}.json"
      },
      "name": "Write symbol's file1",
      "type": "n8n-nodes-base.writeBinaryFile",
      "typeVersion": 1,
      "position": [
        160,
        740
      ]
    },
    {
      "parameters": {
        "executeOnce": false,
        "command": "=if [ -r /home/node/.n8n/binary-data/ipo-checker_{{$node[\"Filter symbols\"].json[\"proposedTickerSymbol\"]}}.json ]; then echo {{$node[\"Filter symbols\"].json[\"proposedTickerSymbol\"]}} exists; else echo {{$node[\"Filter symbols\"].json[\"proposedTickerSymbol\"]}} noexist; fi"
      },
      "name": "Check if file exists for symbol",
      "type": "n8n-nodes-base.executeCommand",
      "typeVersion": 1,
      "position": [
        -520,
        500
      ]
    },
    {
      "parameters": {
        "interval": 3,
        "unit": "hours"
      },
      "name": "How often to run?",
      "type": "n8n-nodes-base.interval",
      "typeVersion": 1,
      "position": [
        -1430,
        610
      ],
      "notesInFlow": true,
      "notes": "(Default: 3 hours)"
    }
  ],
  "connections": {
    "Get IPO Data": {
      "main": [
        [
          {
            "node": "Combine all arrays and add `ipoStatus` key with value",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create JSON-items": {
      "main": [
        [
          {
            "node": "Filter symbols",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write symbol's file": {
      "main": [
        [
          {
            "node": "Read symbol's file - 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read symbol's file": {
      "main": [
        [
          {
            "node": "Move Binary Data1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Move Binary Data1": {
      "main": [
        [
          {
            "node": "IF: ipoStatus is unchanged",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Move Binary Data2": {
      "main": [
        [
          {
            "node": "Route symbols by ipoStatus",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If false reference": {
      "main": [
        [
          {
            "node": "Return API-response data for changed symbols",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If file exists for symbol": {
      "main": [
        [
          {
            "node": "Symbols where file exists",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Symbols where file does not exist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter symbols": {
      "main": [
        [
          {
            "node": "Check if file exists for symbol",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine all arrays and add `ipoStatus` key with value": {
      "main": [
        [
          {
            "node": "Create JSON-items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: ipoStatus is unchanged": {
      "main": [
        [
          {
            "node": "No ipoStatus change",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "If false reference",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read symbol's file - 1": {
      "main": [
        [
          {
            "node": "Move Binary Data2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Move Binary Data0": {
      "main": [
        [
          {
            "node": "Write symbol's file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route symbols by ipoStatus": {
      "main": [
        [
          {
            "node": "Send `Filed` mail",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send `Priced` mail",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send `Upcoming` mail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Return API-response data for changed symbols": {
      "main": [
        [
          {
            "node": "Move Binary Data0",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Symbols where file exists": {
      "main": [
        [
          {
            "node": "Read symbol's file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Function": {
      "main": [
        [
          {
            "node": "IF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF": {
      "main": [
        [
          {
            "node": "NoOp",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "NoOp1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Symbols where file does not exist": {
      "main": [
        [
          {
            "node": "Move Binary Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Move Binary Data": {
      "main": [
        [
          {
            "node": "Write symbol's file1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write symbol's file1": {
      "main": [
        [
          {
            "node": "Read symbol's file - 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check if file exists for symbol": {
      "main": [
        [
          {
            "node": "If file exists for symbol",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "How often to run?": {
      "main": [
        [
          {
            "node": "Get IPO Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {},
  "id": "5"
}

Notes:

  • By default, the workflow runs every 3 hours
  • The Filter symbols node is where you should enter text pertaining to the specific IPOs you wish to track. This will filter the NASDAQ API respond to only the IPOs you’re trying to track.
  • This workflow writes data to a folder called binary-data in the n8n root and stores IPO data here, allowing you to track changes across executions of the workflow. I believe you will need to manually create this folder because n8n can’t do this (unsure?).
  • I removed my email addresses from the send email nodes. You will have to set up credentials and edit those nodes. (If you don’t want to set these credentials up during testing, you can just delete the send mail nodes and replace them with noop).
1 Like

Nice workflow @malasane

Probably another good way to notify new ipos is by Telegram message :slight_smile:

That’s a great suggestion! I don’t use Telegram, but it would work well for people who do.

1 Like