xbrl-queries

star 3

Cypher query patterns for XBRL financial data. Reference doc auto-loaded by neo4j-xbrl agent.

faisalanjum By faisalanjum schedule Updated 3/28/2026

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 as pit in params dict)

Design notes:

  • Per-fact amendment overlay: queries ALL filings per period, newest created wins 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 ASC in 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). The dimensionality column in the output lets the agent verify.
  • Deduplicates: one row per quarter+member after amendment overlay, highest decimals wins.
  • Target-period selector: period_end within 7 days of periodOfReport (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:Revenues
  • us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax
  • us-gaap:CostOfGoodsAndServicesSold
  • us-gaap:GrossProfit
  • us-gaap:OperatingIncomeLoss
  • us-gaap:NetIncomeLoss
  • us-gaap:EarningsPerShareBasic
  • us-gaap:EarningsPerShareDiluted

Balance Sheet

  • us-gaap:Assets
  • us-gaap:Liabilities
  • us-gaap:StockholdersEquity
  • us-gaap:CashAndCashEquivalentsAtCarryingValue
  • us-gaap:AccountsReceivableNetCurrent
  • us-gaap:InventoryNet
  • us-gaap:LongTermDebt

Cash Flow

  • us-gaap:NetCashProvidedByUsedInOperatingActivities
  • us-gaap:NetCashProvidedByUsedInInvestingActivities
  • us-gaap:NetCashProvidedByUsedInFinancingActivities
  • us-gaap:PaymentsOfDividends
  • us-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.created and available_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 produce data[] array
  • Always RETURN items AS data, [] AS gaps
  • Pass pit in the params dict 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() when is_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_CONTEXT relationship; filter with MATCH (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

Install via CLI
npx skills add https://github.com/faisalanjum/EventTrader --skill xbrl-queries
Repository Details
star Stars 3
call_split Forks 1
navigation Branch main
article Path SKILL.md
More from Creator