{
  "id": "rGTB6Zv4mwrrNVA1",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Octagon Stock Quote Skill to Google Sheets",
  "tags": [],
  "nodes": [
    {
      "id": "daa754b6-a730-43ae-82ff-78a810b97bf6",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1600,
        1168
      ],
      "parameters": {
        "width": 480,
        "height": 608,
        "content": "## Octagon Stock Quote Skill to Google Sheets\n\n### How it works\n\n1. Manually triggers the workflow to start.\n2. Fetches stock quote skill details from GitHub.\n3. Reads tickers from a Google Sheet and filters them.\n4. Processes each ticker with an AI prompt and updates the Google Sheet.\n5. Throttles task execution to manage processing load.\n\n### Setup steps\n\n- [ ] Configure GitHub API access for fetching the skill.\n- [ ] Set up Google Sheets API credentials to read and write data.\n\n### Customization\n\nCustomize the filters and AI prompts based on specific stock analysis needs."
      },
      "typeVersion": 1
    },
    {
      "id": "cc1eb228-bfcf-473c-b9dc-e8586a08bd63",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2160,
        1280
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 304,
        "content": "## Manual start trigger\n\nInitiates the workflow manually."
      },
      "typeVersion": 1
    },
    {
      "id": "ccffc61d-2da4-4aad-b3ee-2e3aa029359b",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2432,
        1280
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 304,
        "content": "## Setup task instructions\n\nSets the task and fetches stock quote skill details from GitHub."
      },
      "typeVersion": 1
    },
    {
      "id": "019758b6-ac12-4eb1-ad8f-2727d1a668c7",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2880,
        1280
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 304,
        "content": "## Read and filter tickers\n\nReads tickers from Google Sheets and filters valid ones for further processing."
      },
      "typeVersion": 1
    },
    {
      "id": "0a675422-3eed-41c8-9c38-590cc647def0",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3328,
        1200
      ],
      "parameters": {
        "color": 7,
        "width": 768,
        "height": 384,
        "content": "## Process tickers with AI\n\nLoops through filtered tickers, processes them using AI, and retrieves the stock information."
      },
      "typeVersion": 1
    },
    {
      "id": "1bbcc303-a317-47d5-8007-ffdfd5081e8d",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4304,
        1168
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 400,
        "content": "## Update results and throttle\n\nUpdates the Google Sheet with stock information and throttles execution to control workflow speed."
      },
      "typeVersion": 1
    },
    {
      "id": "399a7264-f4d4-47aa-8381-43ab687640f1",
      "name": "Manual Workflow Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        2208,
        1408
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "437c2711-9059-4e2f-9193-b06537bad029",
      "name": "Set Task and Skills URL",
      "type": "n8n-nodes-base.set",
      "position": [
        2480,
        1408
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "27c3c6bc-7784-4b87-9af4-64aa5912b26f",
              "name": "task",
              "type": "string",
              "value": "Get real-time stock quotes for the tickers in the Google Sheet. For each ticker, build the best Octagon Agent prompt using the fetched stock-quote skill context. Return compact quote fields suitable for updating PRICE and DATE."
            },
            {
              "id": "6464cc3d-eaeb-493c-8b64-a6278bb43de5",
              "name": "skills_url",
              "type": "string",
              "value": "https://raw.githubusercontent.com/OctagonAI/skills/main/skills/stock-quote/SKILL.md"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "2d208d52-9693-4f89-a77c-c63a9d50da17",
      "name": "Filter Valid Stock Tickers",
      "type": "n8n-nodes-base.filter",
      "position": [
        3152,
        1408
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "7e47b022-8b22-4cac-9c3e-72287615ef97",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ ($json.Ticker || $json.TICKER || '').trim() }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "f02a35eb-d53f-4533-99aa-8cd5d763dde2",
      "name": "Loop Over Tickers",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        3376,
        1408
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "be7835f9-b8b8-4ac6-802c-5b59819344eb",
      "name": "Create Octagon API Prompt",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        3600,
        1328
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini"
        },
        "options": {},
        "responses": {
          "values": [
            {
              "content": "={{ \n(() => {\n  const inputs = $('Set Task and Skills URL').first().json;\n  const skillNode = $('Fetch Skill from GitHub URL').first().json;\n\n  const skillContent =\n    typeof skillNode === 'string'\n      ? skillNode\n      : skillNode.data || skillNode.body || skillNode.response || skillNode.text || JSON.stringify(skillNode);\n\n  return `You are a prompt builder for Octagon Agent.\n\nObjective:\nGiven the workflow task, the current Google Sheets row, and the fetched SKILL.md context, produce the exact prompt that should be sent to Octagon Agent.\n\nRules:\n- Output only the final prompt text.\n- Do not include markdown fences.\n- Do not explain your reasoning.\n- Use the stock-quote skill's intended workflow.\n- The prompt must ask Octagon Agent for a real-time stock quote.\n- The prompt must be specific to the ticker in the current row.\n- The prompt should request a compact JSON object that includes at least: ticker, current_price, quote_time, and source/status.\n- If the workflow task asks for extra fields supported by the skill, include them.\n- If the ticker is missing, output: ERROR: Missing ticker.\n\nWorkflow task:\n${inputs.task}\n\nCurrent ticker:\n${$json.Ticker || $json.TICKER || ''}\n\nCurrent Google Sheets row:\n${JSON.stringify($json, null, 2)}\n\nFetched stock-quote SKILL.md from:\n${inputs.skills_url}\n\nFetched stock-quote SKILL.md content:\n${skillContent}`;\n})()\n}}"
            }
          ]
        },
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "273f15f4-4add-4fb3-89ad-b2468d4f0535",
      "name": "Execute Octagon Agent",
      "type": "n8n-nodes-octagon.octagonAgents",
      "position": [
        3952,
        1328
      ],
      "parameters": {
        "query": "={{ \n(() => {\n  const item = $json;\n\n  if (typeof item === 'string') return item;\n\n  if (item.output_text) return item.output_text;\n  if (item.text) return item.text;\n\n  if (item.message?.content) {\n    if (typeof item.message.content === 'string') return item.message.content;\n    if (Array.isArray(item.message.content)) {\n      return item.message.content.map(c => c.text || c.content || '').join('\\n').trim();\n    }\n  }\n\n  if (Array.isArray(item.content)) {\n    return item.content.map(c => c.text || c.content || '').join('\\n').trim();\n  }\n\n  if (item.query) {\n    try {\n      const q = typeof item.query === 'string' ? JSON.parse(item.query) : item.query;\n      const text = q?.output?.[0]?.content?.[0]?.text;\n      if (text) return text;\n    } catch (e) {\n      return item.query;\n    }\n  }\n\n  return JSON.stringify(item);\n})()\n}}",
        "additionalFields": {}
      },
      "credentials": {
        "octagonApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "c41eb433-dc8b-4650-a5b4-632d8962b1d7",
      "name": "Update Tickers in Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        4352,
        1328
      ],
      "parameters": {
        "columns": {
          "value": {
            "Date": "={{ $json.Date }}",
            "Change": "={{ $json.Change }}",
            "Ticker": "={{ $json.Ticker }}",
            "Volume": "={{ $json.Volume }}",
            "Exchange": "={{ $json.Exchange }}",
            "Day Range": "={{ $json['Day Range'] }}",
            "Market Cap": "={{ $json['Market Cap'] }}",
            "52-Week Range": "={{ $json['52-Week Range'] }}",
            "Current Price": "={{ $json['Current Price'] }}",
            "50-Day Average": "={{ $json['50-Day Average'] }}",
            "Previous Close": "={{ $json['Previous Close'] }}",
            "200-Day Average": "={{ $json['200-Day Average'] }}",
            "Octagon Response": "={{ $json['Octagon Response'] }}",
            "Octagon Source/Status": "={{ $json['Octagon Source/Status'] }}"
          },
          "schema": [
            {
              "id": "Ticker",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Ticker",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Current Price",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Current Price",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Change",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Change",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Volume",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Volume",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Day Range",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Day Range",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "52-Week Range",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "52-Week Range",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Market Cap",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Market Cap",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Exchange",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Exchange",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Previous Close",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Previous Close",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Date",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Octagon Response",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Octagon Response",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "50-Day Average",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "50-Day Average",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "200-Day Average",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "200-Day Average",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Octagon Source/Status",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Octagon Source/Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": true,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Ticker"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1onmnZmFFBy3eqSpbIDgrswv1LQ34D1EievWM-B0R4y0/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1onmnZmFFBy3eqSpbIDgrswv1LQ34D1EievWM-B0R4y0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1onmnZmFFBy3eqSpbIDgrswv1LQ34D1EievWM-B0R4y0/edit?usp=drivesdk",
          "cachedResultName": "TICKERS"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "0fb9e984-3765-4f42-9000-eb446ea0cff4",
      "name": "Wait 1 Second",
      "type": "n8n-nodes-base.wait",
      "position": [
        4576,
        1408
      ],
      "parameters": {
        "amount": 1
      },
      "typeVersion": 1.1
    },
    {
      "id": "6c9b80a3-1556-4419-9f9c-11f65f34e8bd",
      "name": "Read Tickers in Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2928,
        1408
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1onmnZmFFBy3eqSpbIDgrswv1LQ34D1EievWM-B0R4y0/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1onmnZmFFBy3eqSpbIDgrswv1LQ34D1EievWM-B0R4y0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1onmnZmFFBy3eqSpbIDgrswv1LQ34D1EievWM-B0R4y0/edit?usp=drivesdk",
          "cachedResultName": "TICKERS"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "5c9af8b2-93e7-4b2d-b5ce-215bad93db30",
      "name": "Fetch Skill from GitHub URL",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2704,
        1408
      ],
      "parameters": {
        "url": "={{ $json.skills_url }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "f25a58c3-9fbd-4bd2-a9e0-96fb37f59d6f",
      "name": "Parse Octagon Quote Response",
      "type": "n8n-nodes-base.code",
      "position": [
        4176,
        1328
      ],
      "parameters": {
        "jsCode": "// Parse Octagon Agent response and merge all quote fields back with the current ticker row.\n// Works with Octagon outputs returned as JSON, markdown tables, plain text, or nested OpenAI-style payloads.\n\nfunction getCurrentTickerRow() {\n  try {\n    return $('Loop Over Tickers').item.json || {};\n  } catch (e) {}\n\n  try {\n    return $('Loop Over Tickers').first().json || {};\n  } catch (e) {}\n\n  return {};\n}\n\nfunction stringifySafe(value) {\n  if (typeof value === 'string') return value;\n  if (value == null) return '';\n  try {\n    return JSON.stringify(value, null, 2);\n  } catch (e) {\n    return String(value);\n  }\n}\n\nfunction unwrapText(value) {\n  if (value == null) return '';\n  if (typeof value === 'string') return value;\n\n  // Common Octagon / agent fields\n  if (typeof value.analysis === 'string') return value.analysis;\n  if (typeof value.output_text === 'string') return value.output_text;\n  if (typeof value.text === 'string') return value.text;\n  if (typeof value.response === 'string') return value.response;\n  if (typeof value.result === 'string') return value.result;\n  if (typeof value.answer === 'string') return value.answer;\n\n  // OpenAI-style message/content arrays\n  if (value.message?.content) {\n    if (typeof value.message.content === 'string') return value.message.content;\n    if (Array.isArray(value.message.content)) {\n      return value.message.content\n        .map(c => c?.text || c?.content || c?.value || '')\n        .join('\\n')\n        .trim();\n    }\n  }\n\n  if (Array.isArray(value.content)) {\n    return value.content\n      .map(c => c?.text || c?.content || c?.value || '')\n      .join('\\n')\n      .trim();\n  }\n\n  // Some Octagon node responses place the generated prompt/result inside query as JSON string\n  if (value.query) {\n    try {\n      const q = typeof value.query === 'string' ? JSON.parse(value.query) : value.query;\n      const text = q?.output?.[0]?.content?.[0]?.text;\n      if (text) return text;\n    } catch (e) {\n      if (typeof value.query === 'string') return value.query;\n    }\n  }\n\n  return stringifySafe(value);\n}\n\nfunction stripCodeFences(text) {\n  return String(text || '')\n    .trim()\n    .replace(/^```json\\s*/i, '')\n    .replace(/^```javascript\\s*/i, '')\n    .replace(/^```\\s*/i, '')\n    .replace(/```$/i, '')\n    .trim();\n}\n\nfunction parseJsonObjectFromText(text) {\n  const cleaned = stripCodeFences(text);\n  if (!cleaned) return {};\n\n  // Try the whole text first\n  try {\n    const parsed = JSON.parse(cleaned);\n    if (parsed && typeof parsed === 'object') return parsed;\n  } catch (e) {}\n\n  // Try each balanced-ish object candidate from first { to each later }\n  const firstBrace = cleaned.indexOf('{');\n  const lastBrace = cleaned.lastIndexOf('}');\n  if (firstBrace >= 0 && lastBrace > firstBrace) {\n    const candidate = cleaned.slice(firstBrace, lastBrace + 1);\n    try {\n      const parsed = JSON.parse(candidate);\n      if (parsed && typeof parsed === 'object') return parsed;\n    } catch (e) {}\n  }\n\n  return {};\n}\n\nfunction flattenObject(obj, prefix = '', out = {}) {\n  if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return out;\n\n  for (const [key, value] of Object.entries(obj)) {\n    const cleanKey = String(key).trim();\n    const path = prefix ? `${prefix}.${cleanKey}` : cleanKey;\n\n    if (value && typeof value === 'object' && !Array.isArray(value)) {\n      flattenObject(value, path, out);\n    } else {\n      out[path] = value;\n      out[cleanKey] = value; // keep simple alias too\n    }\n  }\n\n  return out;\n}\n\nfunction normalizeKey(key) {\n  return String(key || '')\n    .toLowerCase()\n    .replace(/[^a-z0-9]+/g, '_')\n    .replace(/^_+|_+$/g, '');\n}\n\nfunction getFirst(obj, keys, fallback = '') {\n  if (!obj || typeof obj !== 'object') return fallback;\n\n  const flat = flattenObject(obj);\n  const normalized = {};\n\n  for (const [key, value] of Object.entries(flat)) {\n    normalized[normalizeKey(key)] = value;\n  }\n\n  for (const key of keys) {\n    const direct = flat[key];\n    if (direct !== undefined && direct !== null && direct !== '') return direct;\n\n    const norm = normalized[normalizeKey(key)];\n    if (norm !== undefined && norm !== null && norm !== '') return norm;\n  }\n\n  return fallback;\n}\n\nfunction extractMetric(text, metric) {\n  const escapedMetric = metric.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n  // Markdown table row: | Metric | Value |\n  const tableRegex = new RegExp(`\\\\|\\\\s*${escapedMetric}\\\\s*\\\\|\\\\s*([^|]+)\\\\|`, 'i');\n  const tableMatch = String(text || '').match(tableRegex);\n  if (tableMatch) return tableMatch[1].trim();\n\n  // Label/value forms: Metric: value OR Metric - value\n  const labelRegex = new RegExp(`${escapedMetric}\\\\s*[:\\\\-\u2013\u2014]\\\\s*([^\\\\n|]+)`, 'i');\n  const labelMatch = String(text || '').match(labelRegex);\n  if (labelMatch) return labelMatch[1].trim();\n\n  return '';\n}\n\nfunction firstMetric(text, labels) {\n  for (const label of labels) {\n    const value = extractMetric(text, label);\n    if (value) return value;\n  }\n  return '';\n}\n\nconst original = getCurrentTickerRow();\nconst octagonRaw = $input.first().json;\nconst raw = stringifySafe(octagonRaw);\nconst analysis = unwrapText(octagonRaw) || raw;\nconst parsed = parseJsonObjectFromText(analysis);\n\nconst ticker =\n  original.Ticker ||\n  original.TICKER ||\n  getFirst(parsed, ['ticker', 'symbol', 'quote.ticker', 'quote.symbol']) ||\n  firstMetric(analysis, ['Ticker', 'Symbol']);\n\nconst currentPrice =\n  getFirst(parsed, [\n    'current_price', 'currentPrice', 'price', 'last_price', 'lastPrice', 'last',\n    'Last Price', 'Current Price', 'quote.current_price', 'quote.price', 'quote.last_price'\n  ]) ||\n  firstMetric(analysis, ['Last Price', 'Current Price', 'Price', 'Current']);\n\nconst change =\n  getFirst(parsed, ['change', 'price_change', 'priceChange', 'regularMarketChange', 'quote.change']) ||\n  firstMetric(analysis, ['Change', 'Price Change']);\n\nconst volume =\n  getFirst(parsed, ['volume', 'regularMarketVolume', 'quote.volume']) ||\n  firstMetric(analysis, ['Volume']);\n\nconst dayRange =\n  getFirst(parsed, ['day_range', 'dayRange', 'regularMarketDayRange', 'quote.day_range']) ||\n  firstMetric(analysis, ['Day Range', 'Daily Range']);\n\nconst week52Range =\n  getFirst(parsed, [\n    '52_week_range', '52WeekRange', 'fifty_two_week_range', 'fiftyTwoWeekRange',\n    'week_52_range', 'quote.52_week_range'\n  ]) ||\n  firstMetric(analysis, ['52W Range', '52-Week Range', '52 Week Range', '52-week range']);\n\nconst marketCap =\n  getFirst(parsed, ['market_cap', 'marketCap', 'regularMarketMarketCap', 'quote.market_cap']) ||\n  firstMetric(analysis, ['Market Cap', 'Market Capitalization']);\n\nconst exchange =\n  getFirst(parsed, ['exchange', 'exchangeName', 'fullExchangeName', 'quote.exchange']) ||\n  firstMetric(analysis, ['Exchange']);\n\nconst previousClose =\n  getFirst(parsed, ['previous_close', 'previousClose', 'regularMarketPreviousClose', 'quote.previous_close']) ||\n  firstMetric(analysis, ['Previous Close', 'Prev Close']);\n\nconst ma50 =\n  getFirst(parsed, [\n    '50_day_average', 'fifty_day_average', 'fiftyDayAverage', '50_day_ma',\n    'fifty_day_ma', '50DayAverage', 'quote.50_day_average'\n  ]) ||\n  firstMetric(analysis, ['50-Day Average', '50-Day MA', '50-Day Moving Average', '50 Day Average']);\n\nconst ma200 =\n  getFirst(parsed, [\n    '200_day_average', 'two_hundred_day_average', 'twoHundredDayAverage', '200_day_ma',\n    '200DayAverage', 'quote.200_day_average'\n  ]) ||\n  firstMetric(analysis, ['200-Day Average', '200-Day MA', '200-Day Moving Average', '200 Day Average']);\n\nconst date =\n  getFirst(parsed, ['quote_time', 'quoteTime', 'as_of', 'asOf', 'timestamp', 'date', 'time', 'metadata.timestamp']) ||\n  firstMetric(analysis, ['As Of (UTC)', 'As Of', 'Quote Time', 'Timestamp', 'Date']) ||\n  octagonRaw.metadata?.timestamp ||\n  new Date().toISOString();\n\nconst sourceStatus =\n  getFirst(parsed, ['source_status', 'sourceStatus', 'source', 'status', 'provider']) ||\n  firstMetric(analysis, ['Source', 'Status', 'Source/Status']);\n\nreturn [{\n  json: {\n    row_number: original.row_number,\n    Ticker: ticker,\n    'Current Price': currentPrice,\n    Change: change,\n    Volume: volume,\n    'Day Range': dayRange,\n    '52-Week Range': week52Range,\n    'Market Cap': marketCap,\n    Exchange: exchange,\n    'Previous Close': previousClose,\n    '50-Day Average': ma50,\n    '200-Day Average': ma200,\n    Date: date,\n    'Octagon Response': analysis,\n    'Octagon Source/Status': sourceStatus,\n    _parsedQuoteJson: parsed\n  }\n}];"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "fe0e39e0-e779-4f8d-b796-182c33411112",
  "connections": {
    "Wait 1 Second": {
      "main": [
        [
          {
            "node": "Loop Over Tickers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Tickers": {
      "main": [
        [],
        [
          {
            "node": "Create Octagon API Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute Octagon Agent": {
      "main": [
        [
          {
            "node": "Parse Octagon Quote Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Tickers in Sheets": {
      "main": [
        [
          {
            "node": "Filter Valid Stock Tickers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Workflow Trigger": {
      "main": [
        [
          {
            "node": "Set Task and Skills URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Task and Skills URL": {
      "main": [
        [
          {
            "node": "Fetch Skill from GitHub URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Tickers in Sheets": {
      "main": [
        [
          {
            "node": "Wait 1 Second",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Octagon API Prompt": {
      "main": [
        [
          {
            "node": "Execute Octagon Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Valid Stock Tickers": {
      "main": [
        [
          {
            "node": "Loop Over Tickers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Skill from GitHub URL": {
      "main": [
        [
          {
            "node": "Read Tickers in Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Octagon Quote Response": {
      "main": [
        [
          {
            "node": "Update Tickers in Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}