Web functions

FILTERXML

Matches all 83 recorded answers

Recorded: 83 workbook cells. How we measure this · See the Rust code

Local names in 40 languages
LanguageName in Excel
ArabicFILTERXML
BulgarianFILTERXML
CzechFILTERXML
DanishFILTRERXML
GermanFILTERXML
GreekFILTERXML
English (Australia)FILTERXML
English (United Kingdom)FILTERXML
English (United States)FILTERXML
SpanishXMLFILTRO
EstonianFILTERXML
FinnishSUODATA.XML
FrenchFILTRE.XML
HebrewFILTERXML
CroatianFILTERXML
HungarianXMLSZŰRÉS
IndonesianFILTERXML
ItalianFILTERXML
JapaneseFILTERXML
KoreanFILTERXML
LithuanianFILTERXML
LatvianFILTERXML
NorwegianFILTERXML
DutchFILTERXML
PolishFILTERXML
Portuguese (Brazil)FILTERXML
Portuguese (Portugal)FILTRARXML
RomanianFILTERXML
RussianФИЛЬТР.XML
SlovakFILTERXML
SlovenianFILTERXML
Serbian (Latin)FILTERXML
SwedishFILTERXML
ThaiFILTERXML
TurkishXMLFİLTRELE
UkrainianFILTERXML
VietnameseFILTERXML
Chinese (Simplified)FILTERXML
Chinese (Hong Kong)FILTERXML
Chinese (Traditional)FILTERXML

Names from Microsoft's support site.

What it does

FILTERXML(xml, xpath)

FILTERXML is an XPath engine wearing a worksheet function's clothes. It takes an XML document as text and an XPath expression as text, and returns the node set the expression selects. It makes no network request of its own; the network is WEBSERVICE's job.

That said, "bounded" is doing more work here than it does for ENCODEURL. A faithful FILTERXML requires an XML parser and an XPath processor, and Excel's is the Windows one.

FILTERXML(xml, xpath) parses xml and evaluates xpath against it, returning the selected result.

The part that decides how the function behaves in a worksheet is what happens to a node set with more than one node. FILTERXML returns all matching nodes, not the first one: on a modern Excel the result spills down a column, and on a pre-dynamic-array Excel the same formula returns only the top-left value unless entered as an array formula over a range. The same workbook can therefore appear to behave differently on two Excel versions with no change to the formula — this is a spill-adaptation difference at the host boundary (see the call pipeline), not a difference in the function.

The second decisive fact is that the result is text. Nodes selected out of XML come back as strings, whatever they look like. //price returning 12.50 gives you the four-character text 12.5-style content, not a number, and //date gives you an ISO date string, not a serial. Arithmetic downstream needs an explicit conversion, and text that does not read as a number is a named coercion failure rather than a silent zero — see coercion and lifting.

XPath is a real language, not a path syntax: predicates (//item[@id='3']), axes, and the node/attribute distinction (//book/@isbn selects attributes) are all available. That expressiveness is why FILTERXML remains useful even though Power Query supersedes it for serious ingestion work.

Arguments

  • xml (required) — Microsoft: "A string in valid XML format". The whole document, as one text value. In practice that value comes from a WEBSERVICE call or from a cell, and the cell route imposes the 32,767-code-unit text cap described in the value universe. That cap is the practical size limit on what FILTERXML can be given, and it is small for real-world XML.
  • xpath (required) — Microsoft: "A string in standard XPath format".

The argument most often misunderstood is xml, and specifically the assumption that it can be a file. It cannot. There is no path form, no URL form, and no streaming form; the document must already be a string in the worksheet.

The second trap is namespaces. Documents that declare a default namespace need the XPath to account for it, and the usual local-name() workaround is verbose enough that many users conclude FILTERXML is broken when it is behaving correctly.

Try it

Input cells (optional)

Example: {"B2": 3, "B3": 4}. A1 holds the formula. Other cells need values before a formula can read them.

Enter a formula, then calculate.

Runs locally in your browser with OneCalc. Its answer may differ from Excel. Use English function names and commas between arguments.

Quirks

Result and edge cases

Text, one value per selected node, spilling on Excel builds that support dynamic arrays.

  • No match. An XPath that selects nothing. Microsoft's page does not state the result; the Handbook does not know whether it is #VALUE!, #N/A, or an empty result, and will not guess. It is the first probe below.
  • Attribute versus element selection. Both are expressible; whether they are returned identically as text is undocumented.
  • Whitespace and entities. Whether the returned text is the raw node content, the whitespace-normalized string value, or the entity-decoded value is not stated anywhere in the documentation, and the three differ for real documents.

Errors

As documented by Microsoft on the page linked below:

  • #VALUE! — xml is not valid.
  • #VALUE! — xml contains a namespace with a prefix that is not valid.

Both documented conditions concern the document; nothing is documented about a malformed xpath, which is the more common author error. That gap is listed as a probe.

The platform restriction is firmly documented and is arguably the most important fact for a workbook author: FILTERXML is not available in Excel for the web or Excel for Mac. Microsoft adds that it "may appear in the function gallery in Excel for Mac, but it relies on features of the Windows operating system, so it will not return results on Mac". A function present in the gallery that silently does not work is a distinct and badly signposted state, and it makes FILTERXML a poor choice for any workbook that will be shared across platforms.

How to implement

Notes for implementers

  • The XPath dialect and version are unstated. Windows' MSXML supports XPath 1.0; whether Excel exposes anything beyond that is unknown to the Handbook. XPath 1.0 versus 2.0 differ in ways that are observable from a worksheet (sequence handling, string functions, node comparison), so this is not a detail.
  • Namespace handling is the largest single source of behavioural surface, and Microsoft documents exactly one line about it — that an invalid prefix yields #VALUE!. Everything else, including default-namespace behaviour, must be pinned by observation.
  • The result shape is host-adapted. The function returns a node sequence; whether that spills, scalarizes by implicit intersection, or fills an array-entered range is a publication-stage question, not a kernel question.
  • Text normalization (whitespace, entity decoding, CDATA) is a per-node decision that a compatibility implementation must copy rather than choose.
  • A SafePure classification for an XML parser is a strong claim about the parser: no external entity resolution, no DTD fetching, no network. An implementation that hands the document to a permissive parser inherits an entity-expansion attack surface the worksheet author never asked for.

Rust code

Show the CalcFunc source of this function

Quoted from the public repository DnaCalc/DnaCalc, crates calc-func and calc-fml, at commit 9e1884369a67. Comments are the source's own. A kernel shared with other functions appears once, on its module page.

FILTERXML · 1 lines · calc-func/src/catalogue/text_format.rs on GitHub
20 filterxml_fn "FILTERXML" (2, 2) [V "xml", V "xpath"] |s, a, sv| { filterxml(s, a) }
filterxml · 77 lines · calc-func/src/catalogue/text_format.rs on GitHub
/// FILTERXML (dc-uar), ported from OxFunc (web_text_xml_family.rs at 6fca3518, which used
/// sxd-document and sxd-xpath) and fitted to Excel 16.0.20527, measured through the catalogue
/// oracle's dcuar_text rows on 2026-10-29:
///
/// - XML that does not parse (trailing content, a document type declaration) and XPath that does
///   not compile are #VALUE!; a result that is not a node set (`count(//b)`, `string(//b)`,
///   `//b=1`), an empty node set, and a node that is neither an element nor an attribute
///   (`//b/text()`, `//comment()`, `/`) are #VALUE!.
/// - An element's value is its own text children, joined (`<b>x<c>y</c>z</b>` is `xz`); text of
///   white space alone is dropped, as Excel's parser drops it. A node with no value makes the
///   whole call #VALUE!.
/// - Names match by namespace prefix as the document declares them (`//p:b` finds `<p:b>` where
///   `xmlns:p` is declared), and a name without a prefix finds an element of the document's
///   default namespace; a prefix the document does not declare is #VALUE!.
/// - Each value reads as a cell entry would: a number (digits beyond the 15th significant one
///   dropped, `3.14159265358979323` is 3.14159265358979), a date, a time, a percentage, a
///   currency amount or a fraction under the call's locale profile ([`crate::value_text`]),
///   `TRUE` or `FALSE` in any case as a logical; any other text stays text (`#N/A` too).
/// - Several nodes give a column.
fn filterxml(site: &Site<'_>, a: &[CalcValue]) -> Step<CalcValue> {
    let value = |exit| Exit::Error(exit);
    let xml = super::kit::text(site, a, 0)?;
    let path = super::kit::text(site, a, 1)?;
    let declared = filterxml_namespaces(&xml);
    // The default namespace is left out, so that a name without a prefix finds its elements.
    let xml = filterxml_without_default_namespace(&xml);
    for prefix in filterxml_prefixes(&path) {
        if !declared.iter().any(|(declared, _)| *declared == prefix) {
            return fail(WorksheetErrorCode::Value);
        }
    }
    let package = sxd_document::parser::parse(&xml).map_err(|_| value(WorksheetErrorCode::Value))?;
    let document = package.as_document();
    let expression = sxd_xpath::Factory::new()
        .build(&path)
        .ok()
        .flatten()
        .ok_or(value(WorksheetErrorCode::Value))?;
    let mut context = sxd_xpath::Context::new();
    for (prefix, uri) in &declared {
        context.set_namespace(prefix, uri);
    }
    let result = expression
        .evaluate(&context, document.root())
        .map_err(|_| value(WorksheetErrorCode::Value))?;
    let sxd_xpath::Value::Nodeset(nodes) = result else {
        return fail(WorksheetErrorCode::Value);
    };
    let nodes = nodes.document_order();
    if nodes.is_empty() {
        return fail(WorksheetErrorCode::Value);
    }
    let mut cells = Vec::with_capacity(nodes.len());
    for node in nodes {
        let text = match node {
            sxd_xpath::nodeset::Node::Element(element) => element
                .children()
                .into_iter()
                .filter_map(|child| child.text())
                .map(|text| text.text())
                .filter(|text| !text.chars().all(char::is_whitespace))
                .collect::<String>(),
            sxd_xpath::nodeset::Node::Attribute(attribute) => attribute.value().to_owned(),
            _ => return fail(WorksheetErrorCode::Value),
        };
        if text.is_empty() {
            return fail(WorksheetErrorCode::Value);
        }
        cells.push(vec![filterxml_value(site, &text)?]);
    }
    if cells.len() == 1 {
        return Ok(cells.remove(0).remove(0));
    }
    CalcArray::from_rows(cells)
        .map(CalcValue::array)
        .ok_or_else(|| site.not_modelled(None))
}
filterxml_namespaces · 18 lines · calc-func/src/catalogue/text_format.rs on GitHub
/// The namespace prefixes the document declares (`xmlns:p="uri"`), with their URIs.
fn filterxml_namespaces(xml: &str) -> Vec<(String, String)> {
    let mut out = Vec::new();
    let mut rest = xml;
    while let Some(at) = rest.find("xmlns:") {
        rest = &rest[at + 6..];
        let Some(eq) = rest.find('=') else { break };
        let prefix = rest[..eq].trim().to_owned();
        let after = rest[eq + 1..].trim_start();
        let Some(quote) = after.chars().next().filter(|c| *c == '"' || *c == '\'') else {
            continue;
        };
        if let Some(end) = after[1..].find(quote) {
            out.push((prefix, after[1..1 + end].to_owned()));
        }
    }
    out
}
filterxml_without_default_namespace · 23 lines · calc-func/src/catalogue/text_format.rs on GitHub
/// The XML without its default namespace declarations (`xmlns="uri"`).
fn filterxml_without_default_namespace(xml: &str) -> String {
    let mut out = String::with_capacity(xml.len());
    let mut rest = xml;
    while let Some(at) = rest.find("xmlns") {
        let after = &rest[at + 5..];
        let trimmed = after.trim_start();
        if let Some(value) = trimmed.strip_prefix('=') {
            let value = value.trim_start();
            if let Some(quote) = value.chars().next().filter(|c| *c == '"' || *c == '\'')
                && let Some(end) = value[1..].find(quote)
            {
                out.push_str(&rest[..at]);
                rest = &value[1 + end + 1..];
                continue;
            }
        }
        out.push_str(&rest[..at + 5]);
        rest = after;
    }
    out.push_str(rest);
    out
}
filterxml_prefixes · 35 lines · calc-func/src/catalogue/text_format.rs on GitHub
/// The namespace prefixes an XPath expression's names use (`p` of `//p:b`), outside its string
/// literals; an axis (`child::`) is not one.
fn filterxml_prefixes(path: &str) -> Vec<String> {
    let chars: Vec<char> = path.chars().collect();
    let mut out = Vec::new();
    let mut i = 0;
    while i < chars.len() {
        let c = chars[i];
        if c == '\'' || c == '"' {
            i += 1;
            while i < chars.len() && chars[i] != c {
                i += 1;
            }
            i += 1;
            continue;
        }
        if c.is_alphabetic() || c == '_' {
            let start = i;
            while i < chars.len()
                && (chars[i].is_alphanumeric() || matches!(chars[i], '_' | '-' | '.'))
            {
                i += 1;
            }
            if chars.get(i) == Some(&':') && chars.get(i + 1) != Some(&':') {
                out.push(chars[start..i].iter().collect());
            }
            if chars.get(i) == Some(&':') && chars.get(i + 1) == Some(&':') {
                i += 2;
            }
            continue;
        }
        i += 1;
    }
    out
}
filterxml_value · 37 lines · calc-func/src/catalogue/text_format.rs on GitHub
/// A node's value as FILTERXML gives it, read as a cell entry would read it (see [`filterxml`]).
fn filterxml_value(site: &Site<'_>, text: &str) -> Step<CalcValue> {
    let trimmed = text.trim_matches(' ');
    if trimmed.eq_ignore_ascii_case("TRUE") || trimmed.eq_ignore_ascii_case("FALSE") {
        return Ok(CalcValue::logical(trimmed.eq_ignore_ascii_case("TRUE")));
    }
    let shown = crate::catalogue::kit::text_value(text);
    match coerce::number(&crate::catalogue::kit::text_value(&fifteen_digits(trimmed))) {
        Ok(Numeric::Number(n)) => return site.number(n),
        Ok(Numeric::Decimal(decimal)) => {
            if let Ok(n) = decimal.nearest() {
                return site.number(n);
            }
            return Ok(shown);
        }
        Err(_) => {}
    }
    let environment = site.environment().ok_or_else(|| site.not_modelled(None))?;
    let profile = environment
        .profile
        .as_ref()
        .ok_or_else(|| site.not_modelled(None))?;
    let context = ReadContext {
        profile,
        date_system: environment.date_system,
        now: environment.now,
    };
    let units: Vec<u16> = text.encode_utf16().collect();
    if units.len() > crate::value::EXCEL_TEXT_MAX_UTF16_CODE_UNITS {
        return fail(WorksheetErrorCode::Value);
    }
    match value_text::read(&units, &context) {
        Ok(reading) => site.number(reading.value()),
        Err(Unread::Rejected) => Ok(shown),
        Err(Unread::NotModelled) => Err(site.not_modelled(None)),
    }
}
fifteen_digits · 21 lines · calc-func/src/catalogue/text_format.rs on GitHub
/// Decimal text with the digits after its 15th significant one written as 0, as a cell entry
/// keeps 15 (`3.14159265358979323` as `3.14159265358979000`); other text unchanged.
fn fifteen_digits(text: &str) -> String {
    let mut seen = 0;
    let mut in_exponent = false;
    text.chars()
        .map(|c| {
            if c == 'e' || c == 'E' {
                in_exponent = true;
            }
            if in_exponent || !c.is_ascii_digit() {
                return c;
            }
            if seen == 0 && c == '0' {
                return c;
            }
            seen += 1;
            if seen > 15 { '0' } else { c }
        })
        .collect()
}

Kernels shared with other functions

Argument and result plumbing

No implementation has been published with a Handbook test suite for this function.

Version differences

No comparison between specific Excel builds is recorded here yet. This does not establish that all builds behave alike.

Alternatives

Relationships

  • WEBSERVICE — the usual supplier of the xml argument. Microsoft's documented idiom is =FILTERXML(WEBSERVICE("…"&ENCODEURL(C2)), "//path"), which is the three Web functions used as a pipeline: encode, fetch, parse.
  • ENCODEURL — the first stage of that same pipeline.
  • Power Query / Get & Transform — the modern replacement for this workflow, and the one Microsoft steers users toward for anything beyond a single small document. Power Query is not a function, so it does not appear in this catalog, but it is the honest answer to "should I build on FILTERXML?"
  • TEXTSPLIT, TEXTBEFORE, TEXTAFTER — what people use to hack XML apart when FILTERXML is unavailable (notably on Mac). Structurally wrong for XML, but portable.
  • WEBSERVICE returning JSON — a frequent confusion. FILTERXML parses XML only; there is no built-in JSON equivalent in the worksheet function surface.

Named variants

No named variants are recorded yet.

Suggest a correction or contribute an implementation · For agents