Pull live Screener.in key stats for many NSE-listed tickers at once — screening sweeps, delegated subagent tasks ("extract data for these 10 tickers"), watchlist refreshes. Output is a compact per-ticker line, NOT the full Joseph analysis (that lives in indian-stock-analysis). No login needed; standalone page works fine.
The browser is a single session: navigate → extract → next ticker. Batching navigations in one turn fails.
/consolidated/ suffix). If the consolidated page has incomplete/old financial data (e.g., P&L only shows 2015-2016, CAGR fields blank), fall back to standalone (no suffix). Symptoms of bad consolidated data:
This happened with PRECWIRE (Sep 2026): consolidated page was stale, standalone had full 10Y data.
Navigate to https://www.screener.in/company/{TICKER}/ (or /consolidated/). The compact snapshot from browser_navigate contains the summary stat list near the top. Read directly:
The extract_screener_summary.js script regex has repeatedly failed (Aug 2026, Sep 2026) — summary stats return ~ despite documented fixes. Do NOT trust the one-shot regex script for summary stats. Instead, use this reliable two-step approach:
browser_snapshot compact view (they ARE in the top section), OR use the raw text block approach:
(() => {
const b = document.body.innerText;
const extract = (lbl) => {
const i = b.indexOf(lbl);
if (i < 0) return '~';
const m = b.slice(i, i+80).match(/₹\s*([\d,]+(?:\.\d+)?)/);
return m ? m[1].replace(/,/g, '') : '~';
};
const name = (document.querySelector('h1')?.textContent||'').trim().replace(/\s+/g, ' ');
const pidx = b.indexOf('PROS'), cidx = b.indexOf('CONS');
let pros='', cons='';
if (pidx >= 0 && cidx > pidx) pros = b.slice(pidx+4, cidx).split('\n').map(s=>s.trim()).filter(Boolean).join(' | ');
if (cidx >= 0) { const end = b.indexOf('* The pros', cidx); cons = b.slice(cidx+4, end>cidx?end:cidx+2000).split('\n').map(s=>s.trim()).filter(Boolean).join(' | '); }
return JSON.stringify({name, mc:extract('Market Cap'), pe:extract('Stock P/E'), roce:extract('ROCE'), roe:extract('ROE'), dy:extract('Dividend Yield'), pros, cons});
})()
Step 2b: CAGR tables — use the reliable th table-scoped approach (this always works):
(() => { const tbl=(t)=>{const th=[...document.querySelectorAll('th')].find(x=>x.textContent.trim()===t); if(!th)return null; const tb=th.closest('table'); return tb.innerText;}; return JSON.stringify({sales:tbl('Compounded Sales Growth'),profit:tbl('Compounded Profit Growth'),price:tbl('Stock Price CAGR')});})()
Output shape: each table's innerText, e.g. 5 Years:\t27%\n3 Years:\t20%\nTTM:\t0% plus 10 Years (often blank for recently-listed cos — that's normal, report ~). Stock Price CAGR rows are 10Y/5Y/3Y/1 Year — the 1-year figure is the PriceCAGR_1Y field.
3. Assemble the output line in the format the parent requested, e.g.:
TICKER|name|MCap|P/E|ROCE|ROE|DivYld|SalesCAGR_5Y|SalesCAGR_3Y|SalesCAGR_TTM|PAT_CAGR_5Y|PAT_CAGR_3Y|PAT_CAGR_TTM|PriceCAGR_1Y|pros_notes
Use ~ for missing values. Never fabricate.
Price to Earning. If the user gives you a working Screener.in screen query, reproduce it VERBATIM — do not "helpfully" reword field names. This was confirmed when a user's query (Price to Earning < 19) broke after being "corrected" to Stock P/E < 19. When in doubt, use the exact field names from the user's query, not what appears on the page.h2/h3 headings to find the tables returns nothing (the labels live inside table th cells, not as standalone headings — verified empty on all pages). Two validated approaches: (a) th exact-text match + th.closest('table').innerText (CAGR only), or (b) regex over document.body.innerText anchored at section labels — the recommended one-shot script in step 2, which also pulls summary stats + PROS/CONS in the same call.browser_console with (() => { const b=document.body.innerText; const i=b.indexOf('Compounded Sales Growth'); return b.slice(i,i+520); })() — shows the literal 5 Years:\t24% rows, no parsing. Use when a value looks off (e.g. a huge 1-Yr price CAGR). Exception: High 3Y CAGR (300%+) is valid for companies with very low base sales 3 years ago (confirmed for SIGMAADV 331%, KERNEX 497%).browser_snapshot truncates (~15k chars) and the CAGR tables sit in the truncated zone. Do NOT page through the snapshot file (/opt/data/cache/web/browser-snapshot-*.txt) to reach CAGR — the console snippet returns the same data in a few hundred chars. Summary stats, by contrast, ARE in the compact navigate snapshot (top of page).~, don't guess. Confirmed: AEQUS, OMNI, VIDYAWIRES have blank 5Y CAGR due to recent listing./consolidated/ suffix) is the default and matches the task spec "no login needed". If a task needs consolidated figures, use /consolidated/ — same extraction works.extract_screener_summary.js has failed in at least 3 sessions despite multiple documented fixes. The regex pattern with escaped backslashes is fragile across different Screener.in page renders. Always use Step 2a (raw text block with simple indexOf+slice) for summary stats and Step 2b (th table match) for CAGR. Do NOT use the one-shot script for summary stats.web_extract is faster than browser navigation (confirmed working for 5 tickers, Sep 2026) but outputs markdown tables with | separators. The browser extraction script does NOT work with web_extract output — use different regex patterns: r'Dividend\\s+Yield\\s+([0-9.]+)\\s%' for Dividend Yield, and r'5 Years:\\s\\|\\s*([0-9.]+)%' for CAGR in markdown tables. Some summary stats (e.g., Dividend Yield for AZAD) may be missing from web_extract output if the page structure differs — default to 0.00 for missing Dividend Yield. Prefer web_extract for speed; fall back to browser only when summary stats are critical and missing from the markdown output.For ticker lists of 3 or more, web_extract is ~4x faster than browser navigation because it fetches all pages in parallel. Even for small batches (3-5 tickers), web_extract is preferable — it avoids browser session overhead and returns clean markdown. However, the output is markdown with | separators, requiring different parsing.
import re
def parse_screener(content, ticker):
dy = "0.00" # Default if not found
s5 = "~"
s3 = "~"
# Dividend Yield: "Dividend Yield 0.14 %"
dy_match = re.search(r'Dividend\s+Yield\s+([0-9.]+)\s*%', content)
if dy_match:
dy = dy_match.group(1)
# Sales CAGR: markdown table with |
sales_match = re.search(r'Compounded Sales Growth.?\n\| ---.?\n(.*?)(?:Compounded Profit Growth|Stock Price CAGR)', content, re.DOTALL)
if sales_match:
sales_text = sales_match.group(1)
s5_match = re.search(r'5 Years:\s\|\s([0-9.]+)%', sales_text)
s3_match = re.search(r'3 Years:\s\|\s([0-9.]+)%', sales_text)
if s5_match:
s5 = s5_match.group(1)
if s3_match:
s3 = s3_match.group(1)
return dy, s5, s3
Known limitations:
web_extract output if the page structure differs0.00 (company likely doesn't pay dividends)~) = company listed < 5 years ago, not a parsing errorEvery Screener.in company page has an Annual reports subsection under Documents (anchor #documents). Each entry links to the BSE filing PDF. To grab the latest:
1. web_extract the company page (or use browser)
2. Search the markdown for Annual Report — entries look like Annual Report 2026 from bse
3. The highest year number is the latest. Download via curl -L -o annual-report-{year}.pdf <url>
These PDFs are often 200-500+ pages. For topic-specific extraction (e.g., GenAI, ESG, capex), convert to markdown with pymupdf first, then grep — see indian-stock-analysis skill for the full workflow.
NAM-INDIA for Nippon Life) before giving up. Confirmed renames: AMARAJABAT → ARE&M (Amara Raja Energy & Mobility, ex-Amara Raja Batteries, renamed Sep 2023) and DEEPAKNITR → DEEPAKNTR (Deepak Nitrite — correct NSE symbol is DEEPAKNTR).When asked to pull Screener.in summary + growth (CAGR) data for a basket of Indian NSE tickers (list of 5+ tickers, midcap/smallcap screening). This is the fast bulk pass.
For each ticker in the list:
1. Navigate to the standalone page (no login needed):
browser_navigate https://www.screener.in/company/{TICKER}/
The returned snapshot ALREADY contains everything you need in the DOM (even below the fold) — no login required.
2. Summary ratios come free from the snapshot. Read from the right-hand stat list in the navigate result (or the saved snapshot file): Market Cap (₹ Cr), Current Price, Stock P/E, ROCE %, ROE %, Dividend Yield %. Also track 52W High/Low, Book Value, Face Value.
3. Extract CAGR tables via one reusable console snippet. Run scripts/extract_cagr_tables.js (self-contained IIFE) through browser_console(expression=<content>). It returns Compounded Sales Growth, Compounded Profit Growth (each with 10Y/5Y/3Y/TTM), and Stock Price CAGR (incl. 1 Year) as one JSON. This is faster and more reliable than hunting the truncated snapshot for the CAGR rows.
4. PROS/CONS machine text is in the snapshot ("PROS"/"CONS" lists). Note the headline pros (e.g. profit CAGR, debt-free, payout ratio) and cons (e.g. high P/B, no dividend, low ROE).
1. CAGR 10Y / some periods show bare "%" (no number) for recently-listed companies (e.g. KAYNES, MEDANTA listed <10Y). Record the available horizons and note "~" / the gap rather than inventing a 10Y figure.
2. Ticker ↔ company-name mismatch: some tickers resolve to a surprising company name (e.g. MEDANTA → Global Health Ltd, the Medanta hospital chain; NH → Narayana Hrudayalaya). Always report the resolved header name, not just the ticker.
3. Don't add the /consolidated/ suffix for bulk pulls when the standalone page already has all the summary + CAGR data — it's an extra load for no added value in a screening pass.
4. TTM (trailing-twelve-month) can be negative or markedly lower than 3Y/5Y for stocks whose profit growth has stalled (e.g. ABB TTM -13%) — flag these divergence signals rather than smoothing them over.
5. Banks/NBFCs have structurally low ROCE (5-10%). A hard ROCE < 10% filter will eliminate all banks — HDFCBANK, ICICIBANK, SBIN, AXISBANK, KOTAKBANK, CANBK. Banks' business model (deploying deposits as loans) produces lower ROCE than manufacturing/services. When screening a mixed universe, exclude bank/NBFC tickers from ROCE filters, or apply a lower threshold (ROCE < 5%) for financials.
6. Browser connections drop during long bulk runs. When processing 50+ tickers via browser, intermittent "no close frame" errors will occur. Collect failed tickers and retry them in a separate batch — they usually succeed on retry. Don't restart the entire run.
scripts/extract_cagr_tables.js — reusable browser-console IIFE that pulls all CAGR tables in one JSON.