Imported from defog-ai/factiq-plugin (
skills/factiq/SKILL.md). Install upstream withnpx skills add defog-ai/factiq-plugin --skill factiq. Copyright stays with the author.
FactIQ Data Tools
You are the analyst. FactIQ provides authenticated MCP tools for discovery and fetching: catalog, dataset and series search, read-only SQL, series lookup, market data, transcript and media search, satellite signals, style guides, and feedback. There is no server-side agent. You decompose the question, fetch the data, do the math, then answer or build a local output.
Publishing boundary: FactIQ cannot host charts or reports publicly or
create public share links. When asked for a FactIQ-hosted link, explain the
limitation and offer an inline chart or local artifact. Do not inspect or
direct the user to FactIQ's legacy authenticated web interface, use browser
automation, or probe HTTP endpoints to work around a missing tool. The legacy
web interface is not a supported product workflow. Normal OAuth connection is
still supported; website, support, privacy, and OAuth URLs do not imply a
publishing capability. If the user rules out other publishers, do not use
ChatGPT Sites or another service. Never call send_feedback for this
intentional capability boundary.
Three output modes:
- Direct answer — a plain-text sentence with no chart. Use when the question asks for a single current value or a simple scalar lookup where a chart would add nothing: "what's the US unemployment rate right now?", "latest CPI print", "Apple's trailing P/E". Still fetch the value with the MCP tools — only the presentation is a sentence. State the number with its period and source (e.g. "US unemployment was 4.1% in May 2026, per BLS."). The moment the question wants a trend, a history, a comparison across categories or entities, a breakdown, or explicitly asks for a chart or report, switch to one of the modes below.
- Quick chart (
term_chart.py) — one focused local ChartSpec plus an inline terminal preview. Default for a single trend or category comparison. Maps can use a ranked-table terminal fallback; seereferences/output/chart-spec.md. - Detailed report — a saved report JSON object with summary, sections,
charts, methodology, and terminal previews. Use for broad analytical
questions. Covered domains route through
references/report-patterns/README.md. If scope is unclear, usereferences/report-patterns/interview-step.mdfirst.
Data in, output out:
-
All discovery and fetching go through the FactIQ MCP tools. Local scripts build and render the final files from the fetched results.
-
The local scripts never touch the API:
python3 "{plugin_root}/scripts/term_chart.py" render ... # terminal ChartSpec preview python3 "{plugin_root}/scripts/comext_sql.py" ... # SQL generator: Eurostat Comext (EU) trade python3 "{plugin_root}/scripts/trade_sql.py" ... # SQL generator: US/China/India/Korea/Japan/Taiwan customs python3 "{plugin_root}/scripts/hs_codes.py" ... # HS commodity code <-> name, offline python3 "{plugin_root}/scripts/series_math.py" ... # YoY/YTD/share/index/merge on saved resultsResolve
{plugin_root}once, then reuse it. In Claude Code it is${CLAUDE_PLUGIN_ROOT}. In Codex, start from the absolute path supplied for thisSKILL.md: the plugin root is two directories above the directory containing this file (skills/factiq/../..). Never resolve these scripts from the shell's current working directory or from a similarly namedscripts/directory in the user's project. Keep the quotes around the absolute path so installations under a directory containing spaces still work.For any bilateral-trade question, generate the SQL instead of writing it.
comext_sql.pyandtrade_sql.pyencode each schema's series-ID grammar, partner-code system, units, and HS-level rules, so the query is correct by construction — run--helpon either for the subcommands (total / products / trend). Label the HS codes a ranking returns withhs_codes.py(zero server calls), and useseries_math.pyfor growth or shares when you have saved the fetched payload locally.
Setup
One connection covers everything: the FactIQ MCP server bundled with this
plugin (.mcp.json), authorized over OAuth. On first use the coding agent runs
FactIQ's browser-based Connect flow. If the FactIQ tools are missing or
return an auth error, the connection isn't set up yet — tell the user to
authorize the MCP server:
- Claude Code: run
/mcp, pick factiq, and complete the sign-in. - Codex: run
codex mcp login factiqand complete the sign-in.
The same FactIQ login works everywhere (email, Google, or passkey) and authorizes the data and feedback tools.
Local development. The bundled MCP URL is
https://api.factiq.com/mcp. For a local backend, edit .mcp.json in your
development checkout or configure a standalone factiq MCP server in Codex or
Claude Code that points at the local URL.
Tools
All FactIQ tools are MCP tools provided by the factiq MCP server.
Data
| Tool | Purpose |
|---|---|
get_data_catalog (schemas?, full?) |
Per-schema index + the shared table DDL. Call once per session before anything else. full=true returns the heavy per-dataset dump (rarely needed — use describe_dataset). Schemas listed under schemas_without_data have no rows — skip them. |
search_datasets (query, schemas?, limit?) |
Keyword (not semantic) ranking of datasets across all schemas. The first discovery step — find the right schema + dataset_code. |
describe_dataset (schema, dataset_code) |
Full metadata for one dataset: topic, methodology, base-change notice, dimensions, example series. Call after search_datasets. |
search_series (schema, terms, limit?, include_compound?) |
Series-level title-substring search within one schema (terms is a list — prefer short stems). Includes COMPOUND:: series. |
run_sql (schema, sql, question?, explore?, auto_retry?, page?) |
Read-only SELECT against one schema. The power tool for joins, pivots, aggregation. page works on the nasa_fires schema only, where individual rows are the answer; everywhere else, aggregate. |
get_series (schema, series_id, from_year?, to_year?, transform?) |
Fetch one series — timeseries, tabular, or COMPOUND:: ids all work. transform="yoy_pct" (percent change) or "yoy_diff" (difference, for rates) adds a column with the change versus the same period one year earlier, matched by calendar date; the cell is null where that period is absent. A coverage_note with missing_periods means the series skips a period — disclose it. SEC-backed results include row_sources keyed by result_index, with the supporting filing, accession/form/date, reported-vs-derived status, and a standardized source_link. For those series schema="filings" and schema="sec" return the same result. |
get_market_data (asset, data_type?, frequency?, limit?) |
Provider-neutral quotes, daily/weekly/monthly price history, company and ETF profiles, symbol search, FX, and commodities. data_type is price_history, quote, company_profile, etf_profile, or symbol_search; limit is 1–5,000. |
get_geo_data (dataset, region, start_date, end_date, aggregation?, resolution?, include_flares?) |
Satellite-derived signals: fires_viirs (crop burning/wildfires; every detection since 2012 is held in FactIQ's own database, so calls answer in under a second — aggregation="seasons" compares the same calendar window in every year since 2012 in one call, "grid" maps the footprint at a cell size you pick with resolution, "points" returns exact detection coordinates), no2_tropomi / so2_tropomi / co_tropomi (industrial, coal/smelting, and combustion activity), aerosol_index_tropomi (smoke/dust/haze), ndvi_s2 (crop condition) — these five also accept aggregation="grid" for a cell-by-cell spatial snapshot — precip_chirps (0.05° gauge-calibrated rainfall within 50S-50N), precip_imerg (0.1° global rainfall), temperature_power, soil_moisture_power — aggregated over a country, state ("India/Punjab"), or bbox. resolution and include_flares apply to fires_viirs only; gas flares and other permanent industrial heat are excluded unless you ask for them. Read references/data/satellite.md before first use — it covers windows (50 intervals, 200 for fires; grid/points 92 days, 366 for fires), the valid_obs_share rule, and attribution. For fire questions this tool does not cover, query the nasa_fires SQL schema (references/data/schemas.md). |
search_company_filings (company, query?, concept?, search_target?, report_type?, fiscal_year?, fiscal_period?, metric_class?, segment?, date_from?, date_to?, active_only?, format?, limit?) |
The central tool for company filings. Deterministic (no model) search over the structured facts and report metadata in one company's filed reports, for every company FactIQ covers: US SEC filers (10-K, 10-Q, 8-K, plus 20-F/40-F/6-K for foreign filers) and companies listed in Germany (annual, half-year, Q1, and Q3 reports, values in EUR — e.g. company="BAS" for BASF SE), the UK (ULVR for Unilever plc), France (MC for LVMH), Switzerland (NESN for Nestlé), and India (the Nifty 50 companies, from their NSE quarterly results and annual reports — RELIANCE for Reliance Industries; an Indian company's Q4 results hold its audited full year). Use an exact ticker; a share-class sibling (GOOGL for GOOG) resolves to the same filer; a German, UK, French, or Swiss company resolves by its local ticker, full name, or LEI, and carries its exchange suffix (MC.PA, BA.L, ROP.SW) when the bare symbol also belongs to a US filer; an Indian company resolves by its NSE symbol (RELIANCE, M&M), the same symbol with .NS, its BSE scrip code, or its full name; an ambiguous name comes back with possible_matches. Start with search_target="coverage" to see which report types, periods, and metric classes exist. Then set concept to one metric name ("revenue", "net income") to get that concept's values across periods, or use search_target="metrics" to list stored concepts and "facts" for reported values. query is a lexical text search across concept names, source labels, aliases, and segment names; narrow with metric_class (financial, ifrs, segment, geography, product, kpi, apm, guidance), segment, report_type (annual, quarterly, half_year, 10-K, 10-Q), fiscal_year + fiscal_period (2025, Q3), or date_from/date_to. Every result is a tree: company → metric class → concept → series → period. With format="json", filing/fact nodes retain the report URL and add a standardized source_link; exact-ticker metrics/facts misses may fall back to standardized statements with no filing evidence. format="pretty" returns a rendered text tree instead of JSON. When a company reports the same concept twice in one filing, the second copy is labeled "Reported line 2" — never add it to the first. Results stop at limit (max 50) with truncated: true; narrow the filters rather than paging. For joins or aggregations across companies over the same archive, use run_sql on the filings schema (open to every account; tables in references/data/schemas.md). |
search_earnings_transcripts (query, search_target?, ticker?, company_name?, quarter_filter?, claim_family?, section?, detail?, limit?) |
Lexical (not semantic) retrieval over atomic, quote-anchored earnings-call rows — never a raw transcript dump. Tickers go in ticker and company names in company_name; pass one of the two, never both (both ignore case; a name in ticker is still read as a name, and the response says so). company_filter is the old name of ticker and still works. Companies listed in India carry the NSE suffix (RELIANCE.NS, TCS.NS); a plain NSE symbol is also accepted and reported in read_as_exchange_symbol, but a symbol that is also a US ticker (INFY) selects the US listing, so pass INFY.NS for the NSE one. For a non-empty query, strict websearch matches rank above an automatically broadened loose partial-match OR-of-tokens tier, so lower-ranked rows may match only some terms; trigram fallback runs only when full-text search returns no rows. Inspect every row for support and retry concise company-native vocabulary ("capital expenditure", "capex", segment names) before concluding lexical silence. For one-call notes, first use search_target="coverage", choose its exact returned latest_period, then browse claims with query="", that ticker + quarter_filter, detail=true, and a deliberate limit; fetch pressure_points with the same ticker and quarter. The browse is capped, not a promise of a complete call. Claim and pressure rows include transcript_id, source_block_index, qa_turn_id, and source_link; source_link.source_label names the company or ticker, fiscal period, and earnings call transcript, never the ingestion vendor. Quote only verbatim_quote, preserving any […] omission marker exactly; canonical_statement is normalized, and neither analyst_hypothesized nor mgmt_declined_to_confirm is a management assertion. For filed actuals use search_company_filings; for formal targets use sec_guidance. Full target/filter reference and workflows: references/report-patterns/earnings-intelligence.md. |
search_media_appearances (query, search_target?, company?, person?, sort?, appearance_type?, claim_family?, date_from?, date_to?, detail?, limit?, offset?, institution?, country?, show?, channel?, company_filter?) |
Deterministic, lexical retrieval over precomputed public-safe paraphrases of what was said on podcasts, TV interviews, and at conferences outside earnings calls, and of what central banks and monetary authorities published from late 2025 onward: speeches, interviews, blog posts, policy statements, press releases, meeting minutes, press-conference transcripts, testimony before legislatures, and reports from the Federal Reserve Board and the regional Reserve Banks, the European Central Bank, the Bank of England, the Bank of Japan, the Reserve Bank of India, the People's Bank of China, the Bank of Korea, the Hong Kong Monetary Authority, Taiwan's central bank, and the national central banks of the euro area and other EU members. A document written in another language yields English paraphrases; its title may stay in the source language. The speakers are company executives, investors, fund managers, analysts, economists, journalists, central-bank officials, and other guests, and their claims cover listed and unlisted companies (OpenAI, DeepSeek, MiniMax), institutions (central banks, regulators), and whole markets and industries, not only the speaker's own employer; no serving-time model interprets or expands the query. Strict lexical FTS runs first, loose any-term FTS only when strict finds no candidates, and trigram fallback only when both FTS stages are empty. Prefer concise topical language and retry company-native synonyms before concluding silence. Targets are search (default claims + passages blend), claims, passages, pressure_points, appearances, and coverage. sort="relevance" ranks lexical score before publication date; sort="newest" ranks publication date before lexical score. company takes comma-separated tickers and company names together ("NVDA,OpenAI,Federal Reserve"): a value that is a stored ticker matches by ticker, and any other value is matched as the name of a company, institution, or organisation a section is about, ignoring punctuation, a leading "The", and corporate suffixes ("NVIDIA Corp." finds NVIDIA; "Alphabet" also reaches GOOGL rows). Each result records company_tickers, company_names, company_matched, and company_unmatched; a name that matches nothing returns no rows plus up to five possible matches under company_unmatched, not an error, so retry with one of them. The appearances and coverage targets also match every value, upper-cased, against the entity references of claims (products, rivals, partners such as "CUDA"); a value found only there is listed under company_references, and alternate spellings can return different appearances or coverage rows because reference names are matched exactly ("open ai" and "OpenAI" select the same subject but not the same references); the claims rows are the same. company_filter is the old name of company and still works, but do not pass both. company matches what a claim is about, not who published it: company="European Central Bank" also returns a Bank of England speech about the ECB. To select a publisher, use institution, a case-insensitive substring of the institution that published an official document ("Bank of Japan", "Bundesbank"), or country, the ISO code of its jurisdiction ("US", "IN", "EU" for the European Central Bank); either one restricts every target to official documents, and coverage then returns one UNATTRIBUTED row with the document count, date span, and claim count because official documents carry no ticker. person is a case-insensitive speaker-name substring; show is a case-insensitive substring of a podcast show name ("Odd Lots") and returns only podcast episodes; channel is a case-insensitive substring of a YouTube channel name ("Bloomberg Television") and returns only YouTube videos; when more than one of show, channel, and institution is given, rows that match any of them are returned (show="Bloomberg", channel="Bloomberg" returns Bloomberg podcast episodes and Bloomberg YouTube videos in one call); coverage lists the stored show and channel names for each company, so read it first when the exact name is unknown; appearance_type, claim_family, inclusive date_from/date_to, detail, and limit provide further narrowing. limit is one page of 1-50 rows and offset skips that many ranked rows before it; every result carries has_more, and next_offset when more rows follow, so repeat the call with offset=next_offset to read the next page of the same ranking. appearance_type values are podcast, tv_interview, conference, and other for interviews, and for official documents speech (also blog posts and opening remarks), tv_interview (published interviews), statement (also implementation notes), minutes (also summaries of opinions), press_conference, testimony, hearing, and other (press releases, reports, projections). Dates are the video's publication/upload date, not necessarily its recording/event date; an official document also carries event_date, the date of the meeting, speech, or hearing it belongs to (FOMC minutes are published about three weeks after the meeting they describe). claim_family makes blended search claims-only, is invalid with passages, and requires matching claims for catalog targets. Structured finding rows expose result_kind, canonical_paraphrase, speaker/topic/video metadata, relevance, and source_url: a timestamped YouTube URL for a video, the episode page for a podcast, or the document page for an official document (a PDF transcript links to the page, ...pdf#page=4); for an official document, channel is the institution name. appearances returns video-level metadata, attribution, matching-claim count, URL, relevance, and institution, document_kind, and event_date (null on interview rows); coverage returns company-level structured corpus counts and date/channel inventory. detail=true adds normalized claim/attribution fields plus institution, document_kind, and event_date to finding rows, but never raw transcript text or evidence spans; claim-only fields remain null on passages and detail does not change catalog rows. Never put canonical_paraphrase in quotation marks or claim it is verbatim; follow the timestamped or document source when exact wording or tone matters. In a press-conference transcript, a row whose speaker is Questioner is a journalist's question, not the institution's position. Empty-query behavior and the complete workflow are in references/report-patterns/media-intelligence.md. |
search_news (query?, tickers?, topic?, sources?, start_date?, end_date?, sort?, limit?) |
Search FactIQ's curated business-news feed — public RSS headlines and summaries from Bloomberg, the Financial Times, and the Wall Street Journal, plus India-macro (Zerodha Daily Brief, ET HealthWorld) and global-health sources (WHO, ECDC, CDC, STAT News, KFF), aggregated and processed by FactIQ so each article carries the listed companies it names ({symbol, exchange, country}) and an analysis block: searchable keywords, a geography, and an angle — one sentence on why the story matters to an investor. Company stories get analysis too, not just macro ones; only content with no business read at all (sports, lifestyle, celebrity) comes back with analysis: null. Results are headline + short publisher summary + link out, never full articles. query is lexical full-text over headline+summary — start with short concrete stems ("obesity drug", "rate cut"); if a multi-word query matches nothing in full, the tool automatically retries matching ANY of the words with rare words ranked first, flagged as meta.query_mode: "any_term", so one query attempt is usually enough. tickers matches share classes and cross-listings automatically (GOOG also finds GOOGL-tagged articles, TSM its Taiwan listing) — pass whichever symbol you know; most macro stories name no listed company, so zero ticker matches is a normal answer. topic is one of markets / economics / companies / technology / politics / world / energy / health / india / opinion — combined with a query it is a ranking preference (matching sections rank first, but strong matches from other sections still return, since stories often run outside their obvious feed); without a query it filters to the topic's feeds. sort is "latest" (default) or "relevance" (needs a query); limit 1–50 (default 20). Coverage is recent news (most feeds start late 2025) — treat it as a current-events lens, not an archive. |
get_style_guides (guides) |
FactIQ house-style guides ("chart", "report", "sql", "earnings", or "all"). Use these for current style and sourcing rules. Fetch "earnings" before writing from search_earnings_transcripts. |
When an answer uses a quote or filing-backed figure, place that row's provided
source_link.source_url immediately beside the claim as a Markdown link whose
text is source_link.source_label. Do not replace that evidence label with an
ingestion vendor, make a second tool call solely to find a citation, reconstruct
a URL, or substitute a related press release. source_precision="document" is
not exact context. If source_url is null, state that the direct link is
unavailable and use the returned locator/document ID only as provenance
metadata.
Every row-returning tool (run_sql, get_series, search_company_filings,
search_earnings_transcripts, search_media_appearances) returns at most 50
rows, but the remedy is tool-specific:
- For
run_sql, aggregate to the needed grain; for a longget_seriesresult, usefrom_year/to_year. See Context budget below. - For earnings, narrow by ticker, exact fiscal quarter, target, family, and
(for claims) section. Synthesize multiple bounded calls and disclose when a
50-row result may be incomplete. Never query the gated
transcriptsschema withrun_sql, request a raw transcript, or assume pagination exists. - For media, narrow by ticker, person, show, channel, institution, dates,
target, appearance type, and claim family, or read the next page with
offset=next_offsetwhile the result sayshas_more. Synthesize multiple bounded searches; never use SQL or assume a full-transcript path.
There is no universal "give me everything" option, by design.
Earnings target/filter quick reference
| Target | Use and applicable arguments |
|---|---|
claims |
Lexical search or empty browse; company, exact quarter, family (primary or secondary), claims-only section, detail, limit |
pressure_points |
Lexical search or empty browse; company, exact quarter, linked family, detail, limit. section is ignored because these rows are Q&A |
disclosure_profile |
Direct lookup by the first ticker value, the first ticker resolved from company_name, or query; not text or quarter search; other filters, detail, and limit are ignored |
coverage |
Company inventory and limit; query, quarter, family, section, and detail do not narrow it |
Canonical call patterns:
- Latest-call note:
coveragefor one ticker → readlatest_period→ empty-queryclaimswith that ticker + exactquarter_filter,detail=true, deliberate limit →pressure_pointswith the same ticker + quarter. - Cross-company theme: check coverage, then run the same concise query and synonym sweep separately for each ticker + exact comparable quarter; inspect partial-term rows before merging them.
- Disclosure habits: call
disclosure_profilewith one ticker; do not add a quarter or treat the ticker as a theme query.
Quote only verbatim_quote; use canonical_statement unquoted. A […]
inside verbatim_quote marks omitted transcript sentences between
non-adjacent evidence spans — preserve it exactly when quoting, and never
present the text on either side of it as one continuous statement. Treat
analyst_hypothesized as the analyst’s framing and
mgmt_declined_to_confirm as a refusal. Keep spoken call claims, formal
sec_guidance targets, and filed actuals as separate source classes. Put
the row's provided source URL beside every quote you use, with the provided
source label as link text; if it is null, say the link is unavailable instead
of searching for or inventing one.
Media target/filter quick reference
| Target | Use and empty-query behavior |
|---|---|
search |
Default blend of high-signal claims and broad passage cards. Empty query browses recent high-signal claims only, without generic passages |
claims |
Structured, decision-relevant claims by executives, investors, analysts, central-bank officials, and other guests. Empty query browses recent claims |
passages |
Broader substantive topics not promoted to claims. Empty query browses recent passage cards |
pressure_points |
Stored refusal / declined-to-confirm rows, not a complete interviewer-Q&A map. Empty query browses recent refusals |
appearances |
Video-level title, channel, publication date, type, attribution, claim-count, URL, and relevance rows. Empty query browses the catalog |
coverage |
Company-level structured corpus inventory: appearance/claim counts, date span, covered channels, and attribution status. Empty query returns the inventory |
All targets accept company, person, show (podcast show name; returns
only podcast episodes), channel (YouTube channel name; returns only YouTube
videos), appearance_type, claim_family where compatible, publication-date date_from/date_to, limit, and
offset; finding targets also support detail.
institution (publisher-name substring) and country (ISO code, EU for
the European Central Bank) restrict any target to official documents;
when more than one of show, channel, and institution is given, rows
that match any of them are returned;
company matches the subject of a claim, not its publisher.
claim_family suppresses passage cards in search and cannot be combined
with passages. Catalog rows are not expanded by detail=true.
Start with coverage before absence claims. Use search plus
sort="relevance" for a theme sweep, then drill into claims and
passages. Use explicit sort="newest" plus date filters for a timeline.
If a result reaches 50 rows or says has_more, read the next page with
offset=next_offset, or narrow by ticker, person, show, channel, target,
appearance type, institution or country, claim family, or date window.
Never query the gated transcripts schema or ask for a full transcript.
Media findings are sourced paraphrases. Attribute person, company/ticker when
available, publication date, title/channel, and the timestamped link. For an
official document, attribute the institution, the document title, the
publication date, the event_date when it differs, and the document link.
canonical_paraphrase must stay outside quotation marks. Verify the linked
source independently when exact wording or tone is material. For coverage,
theme sweeps, timelines, cross-company work, and media-vs-earnings comparison,
read references/report-patterns/media-intelligence.md before searching.
Feedback
| Tool | Purpose |
|---|---|
send_feedback (message, category?) |
Report a problem to the FactIQ team: category is "data_issue" (a value that contradicts the official source, wrong units/scale, duplicated or missing periods), "tool_error" (a tool that errors or returns malformed results), "missing_data" (advertised but empty, or coverage ends too early), or "other". Returns an acknowledgment. |
Call this when a tool result looks broken. Write one short, specific message with the concrete
identifiers (schema, dataset_code / series_id, the SQL you ran, expected
vs. observed, the official source's value or URL if you have one). Don't
include the user's personal details or your conversation. It's one-way — the
team reviews every report, but nothing comes back — so file it and continue
with the task; never block on it.
Terminal charts — term_chart.py
term_chart.py prints local ANSI/ASCII previews from FactIQ chart objects. It
never calls FactIQ. Build the ChartSpec from fetched data, save it to JSON, and
render it:
python3 "{plugin_root}/scripts/term_chart.py" render --spec /tmp/factiq-chart.json --width 80 --charset ascii --color auto
For a report, save the report object or a wrapper such as
{"question": "...", "report": {...}} to JSON, then render its charts:
python3 "{plugin_root}/scripts/term_chart.py" report --report /tmp/factiq-report.json --width 80 --charset ascii --color auto
After term_chart.py renders, paste the preview verbatim into your reply inside
a triple-backtick code block and provide the saved JSON path.
Supported terminal renderers:
| Renderer | Use when |
|---|---|
bar |
Categorical comparisons and short ranked lists |
line |
Time-series trends (one or more series) |
table |
Fallback for unsupported chart types or dense data |
Useful options:
| Option | Purpose |
|---|---|
--type auto|bar|line|table |
Pick the terminal renderer; auto maps from ChartSpec.type |
--width 80 / --width auto |
Fixed width by default; auto reads the terminal size |
--height N |
Line-chart plot height |
--charset ascii|unicode-block |
Strict ASCII or denser Unicode block glyphs |
--color auto|always|never |
ANSI color control; auto respects TTY, NO_COLOR, and TERM=dumb |
--max-charts N |
Report previews only: cap the number of rendered charts; 0 means all |
--out FILE |
Also save the rendered text |
Because agents often capture command output instead of streaming it directly to
the user's terminal, use --charset ascii --color never for previews you paste
into the final answer. Use ANSI color for real terminal stdout or saved .ansi
previews.
Orchestration workflow
-
Interview before major forks. If the request is broad, vague, or about to become a high-commitment workflow — especially a detailed report or one that could follow multiple scopes — interview the user before fetching data or spawning research subagents. Read
references/report-patterns/interview-step.mdand ask only the few choices that would materially change the work: detail level, audience, user context or hypothesis, priority lens, required/excluded entities, and time window. Pass the answers into all downstream research and assembler prompts as hard context. Skip the interview for direct answers, narrow quick charts, or when the user already gave clear scope, audience, and detail level. If the user does not answer, proceed with the defaults in the interview guide. -
Catalog first. Call
get_data_catalogonce to get the compact per-schema index and the table DDL. It tells you what each schema covers, not every dataset. Skip schemas underschemas_without_data. (You rarely needfull=true; usedescribe_datasetfor detail on one dataset.) -
Find datasets, then drill in. Call
search_datasetsto rank datasets across all schemas by keyword — the primary discovery step. Survey every schema that could be relevant before committing: for India check bothmospiandrbi; for the US checkbls,bea,census; energy meanseia. Once a dataset looks right,describe_datasetfor its dimensions and example series, then find the exact series withsearch_series(substring — prefer short stems likerare, notrare earth) or exploration SQL (run_sqlwithexplore=true) on theseriesanddimensionstables. For multi-source stories, actually fetch data from 2+ schemas. Satellite-derived series live in two schemas:portwatch(daily shipping — chokepoints, ports, country trade estimates) andsatellite(nighttime lights by state, lake/reservoir water levels) — seereferences/data/schemas.mdfor routing andreferences/data/satellite.mdfor the on-demand geo tool. Fire detections live in a third schema,nasa_fires, which holds raw detections rather than series and is shaped unlike the others — read its section inreferences/data/schemas.mdbefore writing SQL against it. To find companies that match conditions (sector, market value, price-to-sales, margins, growth), run onerun_sqlquery on the viewscreener.companies— read Company screener inreferences/data/sql-guide.mdfirst, and stateprice_as_ofin the answer.Eurostat Comext is the exception: country schemas contain millions of series, so do not explore their
seriesordimensionstables by text or dimension value. Read the Eurostat Comext country schemas section inreferences/data/sql-guide.md; it uses the small product lookup table and exact indexed series IDs.The IMF replaces each forecast in place, but the
imfschema also keeps the recent earlier releases of the World Economic Outlook, the Fiscal Monitor, the Regional Economic Outlooks and COFER. Use them for any question about how a forecast has been revised. The plain series id is always the newest release; an earlier one is the same id with the year and month appended (WEO_IND.NGDPD.A_2025OCT), lives in a dataset whose code ends in_vintages, and carries areleasedimension. See IMF past releases inreferences/data/schemas.md.Domain report patterns. If the question is broad and analytical — policy, trade, revenue, investment analysis, "what's driving X" — read
references/report-patterns/README.mdbefore fetching. It teaches the dialectical method every report follows (thesis: the headline reading; antithesis: the strongest contradiction, fetched, not footnoted; synthesis: one claim that explains both) and routes covered domains (bilateral trade, bilateral economic policy, monetary policy, fiscal-policy revenue, business formation) to a playbook of that domain's canonical antitheses with ready SQL. For domains without a playbook, apply the method directly. Either way it changes what you fetch, not just how you write it up. The interview step does not replace this method: it sets the user's preferred scope and audience first, then the report-pattern method determines the thesis, antithesis, synthesis, and data work inside that scope.For report-mode questions covering multiple topics, companies, or data sources, consider decomposing the research into parallel subagents — see Subagent orchestration below.
-
Fetch in batches. Once you know which series you need, issue the fetch calls together (multiple tool calls in one turn). Use
get_seriesfor 1–2 known ids;run_sqlwith a CASE-WHEN pivot for 3+ series or joins. Keep results inside the 50-row cap — aggregate in SQL to the granularity a chart actually needs. For report tables, pick row granularity to fit the window (see the granularity note inreferences/output/report-spec.md). -
Compute deterministically. For YoY/YTD growth, shares, rebasing, or merging compatible results, save the fetched
columnsandresultsas a local FactIQ payload and useseries_math.py. Never inspect Claude/Codex transcripts or session directories to recover tool results. For other metrics such as per-capita values or custom ratios, write a small local Python calculation on the fetched values. There is no server-side code interpreter in this loop. Year-over-year is the same period one year earlier, matched by date, never by row position: for one series useget_serieswithtransform="yoy_pct"(or"yoy_diff"for a rate); for several series or a merged table useseries_math.py yoy; in SQL join on the calendar month (date_trunc('month', prior.time) = date_trunc('month', cur.time) - interval '1 year'— the stored day of the month varies by source, so never compare exact dates), neverLAG(value, 12)(see the trap inreferences/data/sql-guide.md). When a tool result carries acoverage_note, the rows skip the periods inmissing_periods: name them in the answer, do not interpolate, and label any aggregate that spans them as partial ("Q4 2025 average of two months"). -
Recent market data. The DB lags for very recent market/price data — use
get_market_datafor current quotes, commodities, and FX. For what the news is saying about a company, sector, or economy right now, usesearch_news— each business article carries keywords, a geography, and a one-sentence investor angle, plus the tickers it names; chase a company story withget_market_dataorsearch_earnings_transcripts, and a macro story withsearch_series/run_sql. -
Satellite signals. For crop burning, wildfires, air-quality-based activity (NO2/SO2/CO), smoke and dust plumes, crop condition (NDVI), monsoon rainfall, heatwaves, or agricultural drought — where satellite observation runs ahead of official statistics — use
get_geo_data. Readreferences/data/satellite.mdfirst: it covers the ten datasets, region syntax, the window budget (50 intervals, 200 for fires), thegridmode for mapping where a signal sits (fires, NDVI, air quality), the fires-onlypoints,seasons,resolution, andinclude_flarescontrols, cloud-cover caveats, and required attribution. Fires are the one dataset FactIQ stores itself — every detection since 2012 — so a fourteen-year seasonal comparison is one call. Satellite data complements warehouse series; prefer curated series where both exist. -
Answer or render. Direct-answer mode: reply with one sentence that states the number, period, and source. Quick-chart mode: build a ChartSpec from wide-format data (see
references/output/chart-spec.md; required keys aretitle,type,xField,series,data, and a y-axis label goes inyAxisLabel), save it to JSON, runterm_chart.py render, and paste the preview into a fenced code block. Report mode: build and save a report object (seereferences/output/report-spec.md), runterm_chart.py report, and return the findings, local JSON path, and terminal previews.
Subagent orchestration
For report-mode questions that span multiple distinct topics, companies, or data sources, decompose the work into parallel subagents. This does two things: each research thread gets a full, focused context instead of competing for attention in one serial pass, and the report-assembly step gets the spec loaded directly in its prompt so it never guesses at field names.
Before spawning subagents for a broad or underspecified request, run the
interview described in
references/report-patterns/interview-step.md unless the user already gave
clear scope, detail level, audience, and priority lens. Include the interview
answers in every research-agent prompt and in the report-assembler prompt so
the final artifact reflects the user's context instead of only the generic
version of the question.
The interview runs in the main context
Run the interview yourself, in the main conversation — a background subagent cannot put questions to the user. Its job is to clarify the decision, audience, scope, output shape, and success criteria and produce a compact brief before any data is fetched or chart schemas are chosen. Research subagents run only after the brief and the relevant report pattern are known.
Do NOT use subagents for quick-chart mode or single-topic questions — the overhead is not worth it. The decision point is right after step 2 of the orchestration workflow: once you have done the catalog lookup and initial dataset discovery, you know whether the question decomposes into 2+ independent research threads. If it does, fan out.
Research subagents
Spawn one Agent call per research thread. Each agent inherits the skill's FactIQ MCP tools, so it can discover, fetch, and compute on its own. Give each agent a tightly scoped prompt and tell it to return structured findings — not prose and not a final artifact.
Agent prompt template (adapt the specifics per thread):
You are a FactIQ research agent. Answer one sub-question and return structured
findings only. Do not assemble the final chart or report.
Sub-question: {sub_question}
Relevant schemas/datasets (from the parent's catalog step): {hints}
Constraints: every tool result is capped at 50 rows, so aggregate in SQL to
the grain the finding needs; compute derived metrics (YoY, ratios, indices)
yourself from the fetched values. Year-over-year is the same period one year
earlier matched by date (get_series with transform="yoy_pct", series_math.py
yoy, or a SQL join on date_trunc('month', time)), never a 12-row offset or an
exact-date comparison. Report any
coverage_note / missing_periods from the tool results in your findings.
Return your findings as a structured block:
FINDINGS:
- sub_question: (echo it back)
- series_used: [{schema, series_id, title}, ...]
- sql_queries: [the exact SQL you ran, formatted multi-line]
- data: [{columns: [...], rows: [...]}, ...] — the actual fetched/computed values
- key_insights: [1-3 sentences stating what the data shows, with numbers]
- chart_suggestion: {chart_type, title, x_column, y_columns, units}
Launch the agents in parallel — multiple subagent calls in one turn, each
with its own research prompt and a short name such as
research-supply-chain, research-pricing, research-demand.
Each agent runs independently and returns its findings block. Wait for all of them before proceeding to assembly.
Report assembler subagent
After all research is complete, spawn a single report-assembler agent. Its
prompt must contain two things: (1) the full content of references/output/report-spec.md,
so the assembler has the spec without needing the plugin path, and
(2) all the research findings from the previous step.
Before spawning the assembler, read references/output/report-spec.md yourself with
the Read tool. Then embed its entire content in the assembler's prompt.
Agent prompt template:
You are a FactIQ report assembler. Build a complete report object, save it, and
render terminal previews for its charts. Do not do data discovery or fetching;
all data is provided below.
USER QUESTION: {original_question}
=== REPORT SPEC (from references/output/report-spec.md) ===
{paste the full content of references/output/report-spec.md here}
=== END REPORT SPEC ===
=== RESEARCH FINDINGS ===
{paste all findings blocks from the research agents, labeled by thread}
=== END RESEARCH FINDINGS ===
Instructions:
1. Design 2-5 sections. Each section makes one claim its chart(s) prove.
2. Chart titles state the finding with numbers, not the topic.
3. Narratives are plain text — no markdown formatting.
4. Every chart must have columns, data (from the findings above), x_column,
y_columns (for line/bar), sources, and lineage.
5. Lineage code must be formatted multi-line SQL/Python with real newlines.
series_refs must list every series the step used.
6. Save the full report object to JSON and run:
`python3 {plugin_root}/scripts/term_chart.py report --report <json-file> --charset ascii --color never`
7. Return the JSON path and paste the terminal previews into the reply inside a
triple-backtick code block.
Launch the assembler as one subagent (name it report-assembler) with the
spec-plus-findings prompt.
The assembler has the full spec in context, so it builds the report object, saves the JSON, and returns the local path plus terminal previews.
Example decomposition
Question: "How is the US EV market evolving — supply chain, pricing, and demand?"
After step 2 (catalog + discovery), you identify three independent threads:
| Thread | Sub-question | Schemas |
|---|---|---|
| Supply chain | What does US EV battery/component production look like? | census, bea |
| Pricing | How have EV prices and average selling prices changed? | bls (CPI), market data |
| Consumer demand | What are EV sales and registration trends? | bts, bea, market data |
Spawn three research agents in parallel. When all return, spawn one assembler agent with the spec and all three findings blocks. The assembler builds a 3-section report, saves it, renders terminal previews, and returns both.
When NOT to use subagents
- Quick-chart mode (single metric, single chart).
- Single-topic questions even in report mode ("How has US unemployment evolved since 2020?" — one thread, no decomposition needed).
For these cases, do the research and build the output in the main context.
Detailed reports
A report is a structured local research output: a bulleted summary, sections
that pair narrative with charts, and methodology notes. Author every chart row
and narrative claim from data fetched in this session. The JSON format and a
worked example are in references/output/report-spec.md. For reliable assembly,
load that full file into a dedicated report-assembler subagent.
Ground rules:
- 2–5 sections, 1–2 charts each is the normal size. The format allows up to 12 sections, 16 charts). Each section should make one claim its charts prove.
- Chart titles state the finding ("Health care added 652k jobs in 2024 — triple tech's losses"), not the topic ("Jobs by sector").
- Narratives are plain text. Keep them short and direct.
- Cite sources and lineage. Every chart must identify the datasets and the
exact SQL or computation used. Format SQL and Python with real newlines. List
every series used in
series_refs. - Do not pad. If the data only supports one chart, build a quick chart instead of inflating a report.
- Broad analytical questions get the dialectic. Follow the
thesis → antithesis → synthesis method in
references/report-patterns/README.md: sections that only restate the headline reading are an unfinished report. Covered domains (bilateral trade, bilateral economic policy, monetary policy, fiscal-policy revenue, business formation) must additionally meet the required coverage in the playbook the README routes to — do not reduce them to the easiest single chart.
Check the report object against references/output/report-spec.md, save it to
JSON, and render it with term_chart.py report. Return the report findings, the
local JSON path, and visible terminal previews.
Context budget — the 50-row cap
Every row-returning MCP tool (run_sql, get_series) returns at most 50
rows, and there is no "give me everything" option — by design. The cap keeps
results context-sized, so you do not stage data to disk to protect your
context; you take the tool result directly.
There is one exception. In the nasa_fires schema a row is a single satellite
fire detection, which belongs to no series and carries no value to average, so
the rows themselves can be the answer. There run_sql accepts page and walks
the result 50 rows at a time. Give the query an ORDER BY, or the pages will
not line up. No other schema accepts page.
When a result comes back "truncated": true, there is more data and your move
is to aggregate or compute it in SQL, not to try to fetch the raw rows:
- Roll a long daily/monthly series up with
GROUP BY date_trunc('month', time)(or quarter/year) — a chart wants a few hundred points at most, and 50 aggregated points usually says everything. - Return a SUM / AVG / rank / ratio instead of the underlying rows.
- For one series, window it with
get_series(..., from_year=, to_year=), or make a few windowed calls and stitch them.
Whatever you chart or report has to be the aggregated result you bring back — which is also all it needs.
Errors and limits
- MCP tool unavailable / auth error — the FactIQ MCP is not connected. Tell
the user to authorize it (Claude Code:
/mcp→ factiq; Codex:codex mcp login factiq). - 429 — either the 1 request/second rate limit or the monthly tool-call quota. The error states when it resets. Do not re-fetch data you already have.
- 403 — that schema is admin-restricted for this account; drop it.
- SQL errors come back in the tool result as an
error(syntax errors, timeouts, bad column names). Revise the query and rerun. - Zero rows — your filter was too narrow. Broaden it yourself (see
references/data/sql-guide.md).auto_retry=trueopts into a server-side LLM reviser, but you can usually revise better and cheaper yourself. coverage_noteon a result — not an error: the rows skip the periods inmissing_periods(a month the source never published, or one the query window cut off). Any period-over-period figure built with a fixed row offset (LAG(value, 12),shift(12), "the row 12 back") over those rows is wrong for the 12 rows after each gap (a forward offset: the 12 rows before it). Recompute by matching dates, name the missing periods in the answer, and label partial aggregates as partial.- SQL timeout — statements are capped at 30s. Filter on indexed columns
(
series_id,dataset_code) instead of scanning titles, and never pattern-matchseries_idondata_points— resolve ids fromseriesfirst (see the pitfall inreferences/data/sql-guide.md). Foreu_comext_*, do not retry a dimension scan; useeu_comext_lookup.product_codesand exact IDs as described in the Comext section of that guide. - Anything that looks broken on FactIQ's side — a value that contradicts
the official source, wrong units, missing periods, an advertised dataset
that returns nothing, a tool that keeps erroring — report it with
send_feedback(see Feedback above), then work around it and continue. The FactIQ team reviews every report and fixes what it can.
References
references/data/ — the data layer:
schemas.md— what lives in each schema. Theget_data_catalogtool is the live, authoritative version;search_datasets/describe_datasetdrill into individual datasets on demand.sql-guide.md— table structure, query idioms, pitfalls (frequency literals, national vs sub-national, pivots, tabular data).satellite.md— theget_geo_datasatellite tool: datasets and their economic reading, region syntax and coverage, window budgeting, the spatialgridmode, the fires-onlypointsandseasonsmodes and theresolution/include_flarescontrols, cloud/quality caveats, attribution requirements.
references/output/ — local output formats:
chart-spec.md— ChartSpec format, chart-type selection, terminal rendering, and a worked example.report-spec.md— report JSON format: sections, per-chart fields, sources, lineage, limits, and a worked example.
references/report-patterns/ — how to think about broad analytical
questions. Start at report-patterns/interview-step.md when the request is
vague or high-commitment; it defines the interview that clarifies scope and
audience before data work. Then read
report-patterns/README.md: it teaches the dialectical method (thesis →
antithesis → synthesis) that every report follows and routes covered domains
(bilateral trade, bilateral economic policy, monetary policy, fiscal-policy
revenue, business formation, and any added later) to a playbook of that
domain's canonical antitheses with ready SQL. For uncovered domains —
investment analysis, general macro — the README shows how to apply the method
directly.