name: xbrl-queries description: Cypher query patterns for XBRL financial data. Reference doc auto-loaded by neo4j-xbrl agent. user-invocable: false
Neo4j XBRL Queries
Queries for XBRLNode, Fact, Concept, Context, Period, Unit, Dimension, Domain, and Member nodes.
XBRL Labels
| Label | Count | Key properties (type) |
|---|---|---|
| XBRLNode | 8,189 | id, report_id, accessionNo, primaryDocumentUrl (String) |
| Fact | 9,930,840 | value (String), is_numeric/is_nil (String), period_ref, unit_ref |
| Concept | 467,963 | qname, label, type_local, period_type |
| Context | 3,021,535 | context_id, period_u_id, member_u_ids (String) |
| Period | 9,919 | period_type, start_date, end_date (String) |
| Unit | 6,146 | name, namespace, unit_reference (String); is_simple_unit/is_divide (String) |
| Dimension | 878,021 | qname, label, network_uri (String); is_explicit/is_typed (String) |
| Domain | 120,488 | qname, label, level (String) |
| Member | 1,240,344 | qname, label, level (String) |
| Abstract | 50,354 | label, qname (String) |
XBRL Relationships
HAS_XBRL, REPORTS, HAS_CONCEPT, IN_CONTEXT, HAS_PERIOD, HAS_UNIT, FACT_MEMBER, FACT_DIMENSION, HAS_DOMAIN, HAS_MEMBER, PARENT_OF, PRESENTATION_EDGE, CALCULATION_EDGE, FOR_COMPANY.
When to Use XBRL vs Non-XBRL
- Need numeric metrics (EPS, revenue): Use XBRL (10-K/10-Q only).
- Need narrative or 8-K: Use ExtractedSectionContent or ExhibitContent.
- 8-K has no XBRL: Use sections/exhibits instead.
Basic XBRL Queries
Get XBRL node for report
MATCH (r:Report {id: $report_id})-[:HAS_XBRL]->(x:XBRLNode)
RETURN x.id, x.accessionNo, x.primaryDocumentUrl
XBRL reports for company
MATCH (r:Report)-[:PRIMARY_FILER]->(c:Company {ticker: $ticker})
WHERE r.formType IN ['10-K', '10-Q']
MATCH (r)-[:HAS_XBRL]->(x:XBRLNode)
RETURN r.id, r.formType, r.periodOfReport, x.id
ORDER BY r.periodOfReport DESC
Financial Metrics
XBRL metrics (EPS and Revenue, context-safe)
MATCH (r:Report)-[:PRIMARY_FILER]->(c:Company {ticker: $ticker})
WHERE r.formType IN ['10-K','10-Q']
WITH r ORDER BY r.periodOfReport DESC LIMIT 1
MATCH (r)-[:HAS_XBRL]->(x:XBRLNode)<-[:REPORTS]-(f:Fact)
MATCH (f)-[:IN_CONTEXT]->(:Context)
MATCH (f)-[:HAS_CONCEPT]->(con:Concept)
WHERE con.qname IN [
'us-gaap:EarningsPerShareDiluted',
'us-gaap:EarningsPerShareBasic',
'us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax',
'us-gaap:Revenues'
]
AND f.is_numeric = '1'
RETURN con.qname, con.label, f.value, f.period_ref
Get specific metric
MATCH (r:Report)-[:PRIMARY_FILER]->(c:Company {ticker: $ticker})
WHERE r.formType IN ['10-K','10-Q']
WITH r ORDER BY r.periodOfReport DESC LIMIT 1
MATCH (r)-[:HAS_XBRL]->(x:XBRLNode)<-[:REPORTS]-(f:Fact)
MATCH (f)-[:IN_CONTEXT]->(:Context)
MATCH (f)-[:HAS_CONCEPT]->(con:Concept)
WHERE con.qname = $concept_qname // e.g., 'us-gaap:NetIncomeLoss'
AND f.is_numeric = '1'
RETURN con.label, toFloat(f.value) AS value, f.period_ref
Multiple metrics from latest filing
MATCH (r:Report)-[:PRIMARY_FILER]->(c:Company {ticker: $ticker})
WHERE r.formType IN ['10-K','10-Q']
WITH r ORDER BY r.periodOfReport DESC LIMIT 1
MATCH (r)-[:HAS_XBRL]->(x:XBRLNode)<-[:REPORTS]-(f:Fact)
MATCH (f)-[:IN_CONTEXT]->(:Context)
MATCH (f)-[:HAS_CONCEPT]->(con:Concept)
WHERE con.qname IN $concept_list // Pass list of qnames
AND f.is_numeric = '1'
RETURN con.qname, con.label, toFloat(f.value) AS value, f.period_ref
Total vs Segmented Values
Total only (no dimensions)
MATCH (f:Fact)-[:HAS_CONCEPT]->(con:Concept)
WHERE con.label CONTAINS 'Revenue'
AND f.is_numeric = '1'
AND NOT EXISTS((f)-[:FACT_MEMBER]->())
RETURN con.label, f.value LIMIT 10
With dimensions (segmented)
MATCH (f:Fact)-[:FACT_MEMBER]->(m:Member)
MATCH (f)-[:HAS_CONCEPT]->(con:Concept)
WHERE con.label CONTAINS 'Revenue'
AND f.is_numeric = '1'
RETURN m.label AS segment, con.label, f.value LIMIT 10
Segment breakdown for metric
MATCH (r:Report)-[:PRIMARY_FILER]->(c:Company {ticker: $ticker})
WHERE r.formType IN ['10-K','10-Q']
WITH r ORDER BY r.periodOfReport DESC LIMIT 1
MATCH (r)-[:HAS_XBRL]->(x:XBRLNode)<-[:REPORTS]-(f:Fact)
MATCH (f)-[:HAS_CONCEPT]->(con:Concept)
WHERE con.qname = $concept_qname
AND f.is_numeric = '1'
OPTIONAL MATCH (f)-[:FACT_MEMBER]->(m:Member)
RETURN con.label, m.label AS segment, toFloat(f.value) AS value, f.period_ref
ORDER BY segment
Segment values by member across quarters (PIT-safe)
Use when the planner provides exact concept_qname + member_qnames from segment_inventory. Members MUST be from the same axis (e.g., all product members OR all geo members — never mixed). Returns deduplicated quarterly values for specific segment members. Q4 segments may be unavailable for ~94% of companies (10-K has annual-only periods for most).
// Use ALL filings per period (not just best) for per-fact amendment overlay:
// if amendment has the fact, it wins; if only original has it, original comes through.
MATCH (r:Report)-[:PRIMARY_FILER]->(c:Company {ticker: $ticker})
WHERE r.formType IN ['10-Q', '10-K', '10-Q/A', '10-K/A'] AND r.xbrl_status = 'COMPLETED'
AND ($pit IS NULL OR datetime(r.created) <= datetime($pit))
// Limit to the N most recent DISTINCT periods
WITH r.periodOfReport AS period, r
ORDER BY period DESC
WITH collect(DISTINCT period)[..$num_quarters] AS target_periods, collect(r) AS all_filings
UNWIND all_filings AS r
WHERE r.periodOfReport IN target_periods
// Extract segment facts from ALL filings for target periods
MATCH (r)-[:HAS_XBRL]->(x:XBRLNode)<-[:REPORTS]-(f:Fact)
MATCH (f)-[:HAS_CONCEPT]->(con:Concept {qname: $concept_qname})
MATCH (f)-[:FACT_MEMBER]->(m:Member)
MATCH (f)-[:IN_CONTEXT]->(ctx:Context)-[:HAS_PERIOD]->(p:Period)
WHERE m.qname IN $member_qnames
AND p.end_date IS NOT NULL AND p.end_date <> 'null'
// Target period: current quarter only, not prior-year comparative
AND abs(duration.inDays(date(p.end_date), date(r.periodOfReport)).days) <= 7
// Quarterly duration (handles 52-week calendars returning 2 months)
AND duration.between(date(p.start_date), date(p.end_date)).months >= 2
AND duration.between(date(p.start_date), date(p.end_date)).months <= 4
// Note: some companies (e.g., NKE) have multi-axis facts like product×geography.
// The axis-grouped inventory ensures $member_qnames are from ONE axis,
// so cross-axis facts are only returned when the requested member appears
// in a multi-dimensional breakdown. These are valid sub-totals.
// If the agent needs strictly 1D data, add: AND count { (f)-[:FACT_MEMBER]->() } = 1
// Per-fact amendment overlay + dimensionality preference:
// - Fewer FACT_MEMBER rels = more consolidated (1D total > 2D sub-total)
// - Newest filing wins (amendment over original)
// - Highest decimals wins (more precise)
WITH r, r.periodOfReport AS quarter, datetime(r.created) AS filed,
m.qname AS member, m.label AS member_label,
f.value AS value, toInteger(f.decimals) AS dec,
count { (f)-[:FACT_MEMBER]->() } AS member_count
ORDER BY quarter DESC, member, member_count ASC, filed DESC, dec DESC
WITH quarter, member, collect({
available_at: filed,
available_at_source: 'edgar_accepted',
label: member_label,
value: value,
mc: member_count
})[0] AS best
// Wrap in PIT envelope format (data[] + gaps[])
WITH collect({
available_at: toString(best.available_at),
available_at_source: best.available_at_source,
quarter: quarter,
member: member,
member_label: best.label,
value: best.value,
dimensionality: best.mc
}) AS items
RETURN items AS data, [] AS gaps
Parameters:
$ticker(string)$concept_qname(string from segment_inventory, e.g.,'us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax')$member_qnames(list of strings from ONE axis in segment_inventory — never mix product + geo)$num_quarters(int, typically 4-8)$pit(ISO8601 or null for live — matches agent PIT contract, passed aspitin params dict)
Design notes:
- Per-fact amendment overlay: queries ALL filings per period, newest
createdwins per quarter+member. If amendment has the fact, it wins. If only original has it, original comes through. This handles partial amendments correctly. - Multi-axis collapse prevention:
member_count ASCin the ORDER BY ensures 1D facts (totals) are preferred over 2D+ cross-dimensional facts (sub-totals). If SalesCloudMember appears in both a 1-member fact ($2B total) and a 3-member fact (SalesCloud×Americas = $1.5B), the 1-member fact wins. For companies like NKE where geographic segments are ALWAYS 3+ members, those still come through (no 1D alternative exists). Thedimensionalitycolumn in the output lets the agent verify. - Deduplicates: one row per quarter+member after amendment overlay, highest decimals wins.
- Target-period selector:
period_endwithin 7 days ofperiodOfReport(excludes prior-year comparatives). - Q4 limitation: 93.6% of 10-Ks have annual-only periods, so Q4 segment data is often unavailable via this template. Validated across CRM (cloud products), AAPL (iPhone/Mac/Services), AMZN (AWS/North America), WMT (operating segments), with PIT filtering.
Context and Period
Facts with period details
MATCH (f:Fact)-[:IN_CONTEXT]->(ctx:Context)-[:HAS_PERIOD]->(p:Period)
MATCH (f)-[:HAS_CONCEPT]->(con:Concept)
WHERE con.qname = $concept_qname
RETURN con.label, f.value, p.period_type, p.start_date, p.end_date
Facts for specific period
MATCH (f:Fact)-[:IN_CONTEXT]->(ctx:Context)-[:HAS_PERIOD]->(p:Period)
WHERE p.end_date = $period_end_date
AND p.period_type = 'duration'
MATCH (f)-[:HAS_CONCEPT]->(con:Concept)
WHERE f.is_numeric = '1'
RETURN con.qname, con.label, toFloat(f.value) AS value
LIMIT 50
Dimension Analysis
Get dimensions for a fact
MATCH (f:Fact)-[:FACT_DIMENSION]->(dim:Dimension)
MATCH (f)-[:FACT_MEMBER]->(m:Member)
MATCH (f)-[:HAS_CONCEPT]->(con:Concept)
WHERE f.id = $fact_id
RETURN con.label, dim.label AS dimension, m.label AS member, f.value
Available dimensions for concept
MATCH (f:Fact)-[:HAS_CONCEPT]->(con:Concept {qname: $concept_qname})
MATCH (f)-[:FACT_DIMENSION]->(dim:Dimension)
RETURN DISTINCT dim.qname, dim.label
Fulltext Search
Search concepts
CALL db.index.fulltext.queryNodes('concept_ft', $query)
YIELD node, score
RETURN node.qname, node.label, score
ORDER BY score DESC
LIMIT 20
Search fact values (text blocks)
CALL db.index.fulltext.queryNodes('fact_textblock_ft', $query)
YIELD node, score
RETURN node.qname, substring(node.value, 0, 300), score
ORDER BY score DESC
LIMIT 10
Common Concepts (qnames)
Income Statement
us-gaap:Revenuesus-gaap:RevenueFromContractWithCustomerExcludingAssessedTaxus-gaap:CostOfGoodsAndServicesSoldus-gaap:GrossProfitus-gaap:OperatingIncomeLossus-gaap:NetIncomeLossus-gaap:EarningsPerShareBasicus-gaap:EarningsPerShareDiluted
Balance Sheet
us-gaap:Assetsus-gaap:Liabilitiesus-gaap:StockholdersEquityus-gaap:CashAndCashEquivalentsAtCarryingValueus-gaap:AccountsReceivableNetCurrentus-gaap:InventoryNetus-gaap:LongTermDebt
Cash Flow
us-gaap:NetCashProvidedByUsedInOperatingActivitiesus-gaap:NetCashProvidedByUsedInInvestingActivitiesus-gaap:NetCashProvidedByUsedInFinancingActivitiesus-gaap:PaymentsOfDividendsus-gaap:PaymentsForRepurchaseOfCommonStock
Data Analysis
XBRL processing status distribution
MATCH (r:Report) WHERE r.xbrl_status IS NOT NULL
RETURN r.xbrl_status, COUNT(r) as count ORDER BY count DESC
Period type distribution
MATCH (p:Period) RETURN p.period_type, COUNT(p) as count ORDER BY count DESC
Facts by numeric status
MATCH (f:Fact) RETURN f.is_numeric, COUNT(f) as count ORDER BY count DESC
Top concepts by fact count
MATCH (f:Fact)-[:HAS_CONCEPT]->(c:Concept)
RETURN c.label, COUNT(f) as fact_count ORDER BY fact_count DESC LIMIT 20
Context structures coverage
MATCH (ctx:Context)
RETURN COUNT(ctx) as total_contexts,
COUNT(ctx.period_u_id) as has_period,
COUNT(ctx.member_u_ids) as has_members
Dimensions with domain counts
MATCH (d:Dimension) OPTIONAL MATCH (d)-[:HAS_DOMAIN]->(dom)
RETURN d.name, d.label, COUNT(dom) as domain_count
ORDER BY domain_count DESC LIMIT 20
Reports with XBRL by form type
MATCH (r:Report)-[:HAS_XBRL]->(x:XBRLNode)
RETURN r.formType, COUNT(DISTINCT r) as report_count ORDER BY report_count DESC
Recent 10-K XBRL with filing returns
MATCH (c:Company)<-[pf:PRIMARY_FILER]-(r:Report)-[:HAS_XBRL]->(x:XBRLNode)
WHERE r.formType = '10-K' AND datetime(r.created) > datetime() - duration('P180D')
RETURN c.ticker, r.created, r.accessionNo, pf.daily_stock as filing_day_return
ORDER BY r.created DESC LIMIT 20
Count total XBRL nodes
MATCH (x:XBRLNode) RETURN COUNT(x) as total_xbrl_nodes
Companies with recent completed XBRL filings
MATCH (c:Company)<-[:PRIMARY_FILER]-(r:Report)
WHERE r.xbrl_status = 'COMPLETED' AND datetime(r.created) > datetime() - duration('P90D')
RETURN c.ticker, r.formType, r.xbrl_status, r.created
ORDER BY r.created DESC LIMIT 20
Facts with non-null values
MATCH (f:Fact) WHERE f.value IS NOT NULL AND f.value <> '0'
RETURN f.qname, f.value, f.is_numeric LIMIT 20
Facts with numeric values
MATCH (f:Fact) WHERE f.is_numeric = '1' AND f.value IS NOT NULL AND f.value <> '0'
RETURN f.qname, f.value, f.decimals LIMIT 20
Instant vs duration period counts
MATCH (p:Period)
RETURN p.period_type, COUNT(p) as count
ORDER BY count DESC
Measurement units in use
MATCH (u:Unit)
RETURN u.name, u.unit_reference, COUNT(u) as usage_count
ORDER BY usage_count DESC LIMIT 20
Common US-GAAP financial concepts
MATCH (c:Concept) WHERE c.qname STARTS WITH 'us-gaap:'
RETURN DISTINCT c.qname, c.label ORDER BY c.qname LIMIT 30
XBRL nodes with report information
MATCH (r:Report)-[:HAS_XBRL]->(x:XBRLNode)
WHERE r.formType IN ['10-K', '10-Q']
RETURN r.formType, r.accessionNo, x.id, x.cik LIMIT 20
PIT-Safe Envelope Queries
Queries for PIT (Point-in-Time) mode. All use <= $pit on parent Report (boundary-inclusive) and return the standard envelope format. Pass pit in the params dict alongside other Cypher parameters.
XBRL Metrics (PIT)
MATCH (r:Report)-[:PRIMARY_FILER]->(c:Company {ticker: $ticker})
WHERE r.formType IN ['10-K', '10-Q', '10-K/A', '10-Q/A']
AND r.created <= $pit
MATCH (r)-[:HAS_XBRL]->(x:XBRLNode)<-[:REPORTS]-(f:Fact)-[:HAS_CONCEPT]->(con:Concept)
MATCH (f)-[:IN_CONTEXT]->(:Context)
WHERE f.is_numeric = '1'
AND NOT EXISTS { (f)-[:FACT_MEMBER]->() }
OPTIONAL MATCH (f)-[:HAS_PERIOD]->(p:Period)
WITH r, f, con, p ORDER BY r.created DESC, con.qname
WITH collect({
available_at: r.created,
available_at_source: 'edgar_accepted',
accessionNo: r.accessionNo,
formType: r.formType,
concept_qname: con.qname,
concept_label: con.label,
value: f.value,
period_start: p.start_date,
period_end: p.end_date,
period_type: p.period_type
}) AS items
RETURN items AS data, [] AS gaps
Specific Metric (PIT)
MATCH (r:Report)-[:PRIMARY_FILER]->(c:Company {ticker: $ticker})
WHERE r.formType IN ['10-K', '10-Q', '10-K/A', '10-Q/A']
AND r.created <= $pit
MATCH (r)-[:HAS_XBRL]->(x:XBRLNode)<-[:REPORTS]-(f:Fact)-[:HAS_CONCEPT]->(con:Concept)
MATCH (f)-[:IN_CONTEXT]->(:Context)
WHERE con.qname = $concept_qname
AND f.is_numeric = '1'
AND NOT EXISTS { (f)-[:FACT_MEMBER]->() }
OPTIONAL MATCH (f)-[:HAS_PERIOD]->(p:Period)
WITH r, f, con, p ORDER BY r.created DESC
WITH collect({
available_at: r.created,
available_at_source: 'edgar_accepted',
accessionNo: r.accessionNo,
formType: r.formType,
concept_qname: con.qname,
concept_label: con.label,
value: f.value,
period_start: p.start_date,
period_end: p.end_date,
period_type: p.period_type
}) AS items
RETURN items AS data, [] AS gaps
Concept Search (PIT)
CALL db.index.fulltext.queryNodes('concept_ft', $query)
YIELD node, score
MATCH (f:Fact)-[:HAS_CONCEPT]->(node)
MATCH (f)-[:REPORTS]->(x:XBRLNode)<-[:HAS_XBRL]-(r:Report)-[:PRIMARY_FILER]->(c:Company {ticker: $ticker})
MATCH (f)-[:IN_CONTEXT]->(:Context)
WHERE r.formType IN ['10-K', '10-Q', '10-K/A', '10-Q/A']
AND r.created <= $pit
OPTIONAL MATCH (f)-[:HAS_PERIOD]->(p:Period)
WITH r, f, node, p, score ORDER BY score DESC LIMIT 20
WITH collect({
available_at: r.created,
available_at_source: 'edgar_accepted',
accessionNo: r.accessionNo,
formType: r.formType,
concept_qname: node.qname,
concept_label: node.label,
value: f.value,
period_start: p.start_date,
period_end: p.end_date,
period_type: p.period_type,
ft_score: score
}) AS items
RETURN items AS data, [] AS gaps
PIT-Safe Query Rules
- Filter on parent Report:
r.created <= $pit(XBRL has no own timestamp) - NEVER include PRIMARY_FILER relationship properties (daily_stock, daily_macro, etc.)
- Always include
available_at: r.createdandavailable_at_source: 'edgar_accepted' - XBRL in 10-K/10-Q and their amendments: always
WHERE r.formType IN ['10-K','10-Q','10-K/A','10-Q/A'] - Use
collect({...})to producedata[]array - Always
RETURN items AS data, [] AS gaps - Pass
pitin theparamsdict alongside other Cypher parameters - If query returns 0 results, the envelope
{"data":[],"gaps":[]}passes the gate
Notes
- Fact.value is String even for numeric facts; use
toFloat()whenis_numeric='1'. - is_numeric/is_nil are string booleans ('0'/'1').
- Dimension.is_explicit/is_typed are string booleans ('0'/'1').
- Unit.is_simple_unit/is_divide are string booleans ('0'/'1').
- Some Facts lack Context: 12,939 Facts have no
IN_CONTEXTrelationship; filter withMATCH (f)-[:IN_CONTEXT]->(:Context). - Period.end_date can be string 'null' for
period_type='instant'(2,776 rows). - XBRL only in 10-K/10-Q and their amendments (10-K/A, 10-Q/A): 8-K filings have no XBRL data.
- Fulltext indexes:
concept_ft(label/qname),fact_textblock_ft(value/qname),abstract_ft(label).
Known Data Gaps
| Date | Gap | Affected | Mitigation |
|---|---|---|---|
| 2026-01-11 | Revenue values comma-formatted | HRL Revenue facts | Use f.value as string; toFloat() fails on "2,898,810,000" |
Version 1.2 | 2026-02-15 | Fixed PIT queries: corrected graph path (Fact-[:REPORTS]->XBRLNode, Fact-[:HAS_CONCEPT]->Concept), added Period joins