#Output Instructions
These instructions control what your stylesheet produces — text, typed values, comments, processing instructions, and diagnostic messages. If templates are the structure of your transformation, output instructions are the bricks.
#Contents
#xsl:value-of
Creates a text node from the string value of an expression. This is the workhorse instruction for producing text output.
#Basic Usage
<xsl:template match="product">
<p>
<xsl:value-of select="name"/>: $<xsl:value-of select="price"/>
</p>
</xsl:template>Given <product><name>Widget Pro</name><price>29.99</price></product>, this produces:
<p>Widget Pro: $29.99</p>
C# parallel: This is like Razor's @Model.Name — it evaluates an expression and inserts the string value into the output.
#The separator Attribute
When select evaluates to a sequence of multiple items, separator controls how they are joined. The default separator is a single space in XSLT 2.0+ (it was undefined in XSLT 1.0, which only took the first item).
<!-- Input: multiple <tag> children -->
<product>
<tag>electronics</tag>
<tag>sale</tag>
<tag>new</tag>
</product>
<!-- Join with comma-space -->
<xsl:value-of select="tag" separator=", "/>
<!-- Output: electronics, sale, new -->
<!-- Join with pipe -->
<xsl:value-of select="tag" separator=" | "/>
<!-- Output: electronics | sale | new -->
<!-- No separator — concatenate directly -->
<xsl:value-of select="('A','B','C')" separator=""/>
<!-- Output: ABC -->
C# parallel: string.Join(", ", tags)
#Content Instead of select
You can put a sequence constructor inside xsl:value-of instead of using select. The output of the constructor is atomized and converted to a string:
<xsl:value-of>
<xsl:text>Order #</xsl:text>
<xsl:value-of select="@id"/>
<xsl:text> placed on </xsl:text>
<xsl:value-of select="format-date(@date, '[MNn] [D], [Y]')"/>
</xsl:value-of>This is rarely needed — string concatenation in the select attribute or literal text is usually cleaner.
#value-of vs. sequence
This is a critical distinction:
|
|
|
|
|---|---|---|
|
Returns |
Always a text node (string) |
The original typed value |
|
Sequences |
Joins items with separator |
Returns the sequence as-is |
|
Nodes |
Extracts string value |
Returns the node itself |
|
Use in functions |
Rarely correct |
Almost always what you want |
<!-- value-of stringifies everything -->
<xsl:value-of select="42"/> <!-- text node "42" -->
<xsl:value-of select="(1, 2, 3)"/> <!-- text node "1 2 3" -->
<!-- sequence preserves types -->
<xsl:sequence select="42"/> <!-- xs:integer 42 -->
<xsl:sequence select="(1, 2, 3)"/> <!-- sequence of three integers -->
Rule of thumb: Use xsl:value-of when you are producing text for output. Use xsl:sequence when you are returning values from functions or building sequences for further processing.
#xsl:text
Produces a text node with exact whitespace control. While you can write literal text directly in a template, xsl:text gives you precise control over what whitespace appears in the output.
#Why xsl:text Exists
In XSLT, whitespace between instructions is significant — it ends up in the output. Consider:
<xsl:template match="product">
<span>
<xsl:value-of select="name"/>
-
<xsl:value-of select="@sku"/>
</span>
</xsl:template>This produces <span>\n Widget Pro\n -\n WP-001\n </span> with newlines and indentation — usually not what you want. Using xsl:text lets you control the output exactly:
<xsl:template match="product">
<span>
<xsl:value-of select="name"/>
<xsl:text> - </xsl:text>
<xsl:value-of select="@sku"/>
</span>
</xsl:template>Now the output is <span>Widget Pro - WP-001</span> (plus the whitespace around the span tags, but the content between the text nodes is controlled).
For fully clean output, wrap everything:
<xsl:template match="product">
<span><xsl:value-of select="name"/><xsl:text> - </xsl:text><xsl:value-of select="@sku"/></span>
</xsl:template>
<!-- Output: <span>Widget Pro - WP-001</span> -->
C# parallel: Think of xsl:text like a @: line in Razor — it explicitly marks content as text output rather than code.
#disable-output-escaping
The disable-output-escaping attribute (abbreviated doe in some discussions) tells the serializer not to escape special characters:
<!-- Normal behavior -->
<xsl:text><br/></xsl:text>
<!-- Output: <br/> (escaped, appears as literal text in browser) -->
<!-- With disable-output-escaping -->
<xsl:text disable-output-escaping="yes"><br/></xsl:text>
<!-- Output: <br/> (raw markup injected into output) -->
Warning: disable-output-escaping is widely considered an anti-pattern. It breaks the tree model — you are injecting raw characters into what should be a well-formed node tree. It does not work in all serialization scenarios and is not supported by all processors. If you need to produce raw markup, consider using xsl:output method="html" or building proper nodes with xsl:element.
C# parallel: Html.Raw() in Razor — same capability, same "use with caution" advice.
#Outputting Special Characters
xsl:text is the standard way to output characters that would be awkward as literal text:
<!-- Newline -->
<xsl:text> </xsl:text>
<!-- Tab -->
<xsl:text>	</xsl:text>
<!-- Non-breaking space -->
<xsl:text> </xsl:text>
<!-- Combining them -->
<xsl:text>Name	Price	Stock </xsl:text>#xsl:sequence
Returns a value — any value — without converting it to a string. This is the most important output instruction for XSLT 2.0+ and is essential for writing functions.
#Basic Usage
<xsl:sequence select="42"/> <!-- returns integer 42 -->
<xsl:sequence select="'hello'"/> <!-- returns string "hello" -->
<xsl:sequence select="true()"/> <!-- returns boolean true -->
<xsl:sequence select="(1, 2, 3)"/> <!-- returns sequence of 3 integers -->
<xsl:sequence select="//product"/> <!-- returns sequence of product nodes -->#Why xsl:sequence Matters for Functions
When you write an xsl:function, the return value is the sequence constructed by the function body. If you use xsl:value-of, you always get a text node — even if you wanted a number or a boolean:
<!-- WRONG: returns a text node "42", not the integer 42 -->
<xsl:function name="my:answer" as="xs:integer">
<xsl:value-of select="42"/>
</xsl:function>
<!-- RIGHT: returns the integer 42 -->
<xsl:function name="my:answer" as="xs:integer">
<xsl:sequence select="42"/>
</xsl:function>
C# parallel: xsl:sequence is like a return statement. xsl:value-of is like return value.ToString() — it works, but you lose the type.
#Returning Nodes
xsl:sequence returns a reference to existing nodes, while xsl:copy-of creates deep copies. This matters for identity comparisons and performance:
<!-- Returns a reference to the original node -->
<xsl:sequence select="//product[@id='WP-001']"/>
<!-- Returns a deep copy (new node, different identity) -->
<xsl:copy-of select="//product[@id='WP-001']"/>#Building Sequences
You can build sequences incrementally:
<xsl:function name="my:price-range" as="xs:decimal+">
<xsl:param name="products" as="element(product)*"/>
<xsl:sequence select="min($products/price)"/>
<xsl:sequence select="max($products/price)"/>
</xsl:function>
<!-- Usage: -->
<!-- my:price-range(//product) returns (9.99, 299.99) -->#In Template Bodies
Inside a template, xsl:sequence can return nodes for further processing:
<xsl:variable name="active-products" as="element(product)*">
<xsl:sequence select="//product[@status='active']"/>
</xsl:variable>
<!-- The variable holds references to the original nodes -->
<xsl:for-each select="$active-products">
<!-- context node is the original product element, with all its relationships intact -->
<xsl:value-of select="ancestor::category/name"/>
</xsl:for-each>#xsl:comment
Generates an XML or HTML comment in the output.
<xsl:comment>Generated by ProductCatalog.xslt on <xsl:value-of select="current-dateTime()"/></xsl:comment>Output:
<!-- Generated by ProductCatalog.xslt on 2026-03-19T14:30:00 -->#Dynamic Comments
The content is a sequence constructor, so you can use any XSLT instruction inside:
<xsl:comment>
<xsl:text>Product count: </xsl:text>
<xsl:value-of select="count(//product)"/>
<xsl:text> | Categories: </xsl:text>
<xsl:value-of select="count(//category)"/>
</xsl:comment>
<!-- Output: <!-- Product count: 42 | Categories: 5 --> -->#Practical Uses
-
Debug markers: Inject comments to trace which template produced which output
-
Build metadata: Stamp generation time, source file, or version info
-
Conditional comments for IE: (historical, but still found in legacy code)
<!-- Debug: mark template boundaries -->
<xsl:template match="product">
<xsl:comment>BEGIN product <xsl:value-of select="@id"/></xsl:comment>
<div class="product">
<xsl:apply-templates/>
</div>
<xsl:comment>END product <xsl:value-of select="@id"/></xsl:comment>
</xsl:template>
C# parallel: <!-- --> in Razor, or @* *@ for Razor comments (which don't appear in output).
Note: The XSLT processor will automatically prevent -- from appearing inside the comment content (it would break well-formedness). If your content contains --, a space is inserted to produce - -.
#xsl:processing-instruction
Generates a processing instruction in the output.
<xsl:processing-instruction name="xml-stylesheet">
<xsl:text>type="text/css" href="catalog.css"</xsl:text>
</xsl:processing-instruction>Output:
<?xml-stylesheet type="text/css" href="catalog.css"?>#Dynamic PI Names
The name attribute can be an attribute value template:
<xsl:processing-instruction name="{$pi-name}">
<xsl:value-of select="$pi-content"/>
</xsl:processing-instruction>#Practical Uses
Processing instructions are relatively uncommon in modern XML, but you may encounter them for:
-
Stylesheet associations:
<?xml-stylesheet?>in XML documents -
Application-specific directives: Some systems use PIs for page breaks, soft hyphens, or other rendering hints
-
PHP-style template markers: If you are generating PHP output from XSLT
<!-- Generate a PHP include -->
<xsl:processing-instruction name="php">
<xsl:text>include 'header.php';</xsl:text>
</xsl:processing-instruction>
<!-- Output: <?php include 'header.php';?> -->
Note: You cannot generate the XML declaration (<?xml version="1.0"?>) with xsl:processing-instruction — use xsl:output for that.
#xsl:message
Sends a message to the XSLT processor's message output — typically the console or a log. This is your primary debugging tool.
#Basic Diagnostic Output
<xsl:template match="product">
<xsl:message>Processing product: <xsl:value-of select="@id"/> - <xsl:value-of select="name"/></xsl:message>
<!-- ... normal template body ... -->
</xsl:template>The message appears on stderr (or the processor's message handler) but does not appear in the transformation output. This is exactly like Console.Error.WriteLine() or Debug.WriteLine() in C#.
#The terminate Attribute
Setting terminate="yes" turns the message into a fatal error — the transformation stops immediately:
<xsl:template match="product[not(@id)]">
<xsl:message terminate="yes">
FATAL: Product element at line <xsl:value-of select="saxon:line-number(.)"/>
is missing required @id attribute.
</xsl:message>
</xsl:template>
C# parallel: throw new InvalidOperationException("...") — terminate="yes" is an exception you cannot catch (in XSLT 2.0). In XSLT 3.0, it raises a recoverable error that xsl:try/xsl:catch can handle.
#The select Attribute
Instead of using content, you can use select for simple messages:
<xsl:message select="'Entering catalog template'"/>
<xsl:message select="concat('Product count: ', count(product))"/>#The error-code Attribute (XSLT 3.0)
You can specify an error code for programmatic error handling:
<xsl:message terminate="yes" error-code="Q{http://example.com/errors}MISSING-ID">
Product is missing required @id attribute.
</xsl:message>This error code can be caught by xsl:catch:
<xsl:try>
<xsl:apply-templates select="//product"/>
<xsl:catch errors="Q{http://example.com/errors}MISSING-ID">
<xsl:message>Skipping product with missing ID</xsl:message>
</xsl:catch>
</xsl:try>#Debugging Patterns
Use xsl:message liberally during development:
<!-- Trace template entry/exit -->
<xsl:template match="product">
<xsl:message select="concat('[ENTER] product template, id=', @id)"/>
<div class="product">
<xsl:apply-templates/>
</div>
<xsl:message select="concat('[EXIT] product template, id=', @id)"/>
</xsl:template>
<!-- Dump variable values -->
<xsl:variable name="total" select="sum(//product/price)"/>
<xsl:message select="concat('Total price: ', $total)"/>
<!-- Conditional debug (controlled by a parameter) -->
<xsl:param name="debug" select="false()" static="yes"/>
<xsl:template match="product">
<xsl:if test="$debug" use-when="$debug">
<xsl:message select="concat('DEBUG: product ', @id)"/>
</xsl:if>
<!-- ... -->
</xsl:template>C# parallel:
|
XSLT |
C# |
|---|---|
|
|
|
|
|
|
|
|
|
#Putting It All Together
Here is a realistic template that uses several output instructions together to produce an HTML product card:
<xsl:template match="product">
<xsl:comment>Product: <xsl:value-of select="@id"/></xsl:comment>
<div class="product-card">
<h3><xsl:value-of select="name"/></h3>
<p class="price">
<xsl:text>$</xsl:text>
<xsl:value-of select="format-number(price, '#,##0.00')"/>
</p>
<xsl:if test="tag">
<p class="tags">
<xsl:value-of select="tag" separator=" | "/>
</p>
</xsl:if>
</div>
</xsl:template>Given:
<product id="WP-001">
<name>Widget Pro</name>
<price>29.99</price>
<tag>electronics</tag>
<tag>sale</tag>
</product>Output:
<!-- Product: WP-001 -->
<div class="product-card">
<h3>Widget Pro</h3>
<p class="price">$29.99</p>
<p class="tags">electronics | sale</p>
</div>