Skip to content

Built-in Functions

XLFill includes 16 built-in functions that you can use directly in ${...} expressions. No registration needed — they’re available in every template.

Every function below shows:

  • Syntax — what the call looks like in a template cell
  • Examples — typical inputs and outputs
  • (Some entries) Walkthrough — a paragraph showing where you’d use it in a real report

If you’re new to expressions, start with the Expressions guide — it covers the grammar (${...}, dot access, ternary, etc.) that hosts these functions.

Looking for the big picture (commands + functions + Go code together)? The Beginner Tutorial walks through everything end-to-end.

Converts a string to uppercase.

${upper(e.Name)} → "ALICE JOHNSON"
${upper(e.Department)} → "ENGINEERING"

Converts a string to lowercase.

${lower(e.Email)} → "alice@example.com"
${lower(e.Status)} → "active"

Converts a string to title case (first letter of each word capitalized).

${title(e.Name)} → "Alice Johnson"
${title("hello world")} → "Hello World"

Joins a slice of values into a single string with a separator.

${join(", ", e.Skills)} → "Go, Python, SQL"
${join(" | ", e.Departments)} → "Engineering | Research"
${join("\n", e.Notes)} → multi-line text

Useful for combining list data into a single cell value.

Walkthrough — collapse skill lists into one cell

You have an employee record where Skills is []string{"Go", "Python", "SQL"}. You don’t want three rows per employee; you want all the skills in one cell. join(", ", e.Skills) gives you "Go, Python, SQL" in a single cell, comma-separated. Combine with formatNumber(len(e.Skills), 0) for a count, and you can write a header like "Skills (3)" alongside the list.

Formats a number with comma thousands-separators and a given number of decimal places.

${formatNumber(e.Salary, 0)} → "75,000"
${formatNumber(e.Salary, 2)} → "75,000.00"
${formatNumber(e.Rating, 1)} → "4.5"
${formatNumber(-1234567.89, 2)} → "-1,234,567.89"

decimals is an integer count of digits after the decimal point.

Formats a date/time value using Go’s time layout syntax.

${formatDate(e.HireDate, "2006-01-02")} → "2024-03-15"
${formatDate(e.HireDate, "January 2, 2006")} → "March 15, 2024"
${formatDate(e.HireDate, "02/01/2006")} → "15/03/2024"
${formatDate(e.CreatedAt, "2006-01-02 15:04:05")} → "2024-03-15 09:30:00"

Uses Go’s reference time (Mon Jan 2 15:04:05 MST 2006) — see Go time package for layout options.

Walkthrough — date in the report header

Your data map has "reportDate": time.Now(). In your template, cell A1 reads:

Generated on ${formatDate(reportDate, "January 2, 2006 at 3:04 PM")}

Output: Generated on May 21, 2026 at 4:18 PM. Same template, used daily, always shows the current date in a human-friendly format.

Returns the first non-nil, non-empty value from the arguments.

${coalesce(e.Nickname, e.FirstName, "Unknown")} → first non-empty value
${coalesce(e.Phone, e.Mobile, "N/A")} → fallback chain

Like SQL’s COALESCE — essential for handling optional fields without template errors.

Returns fallback if value is nil, empty string, or zero value; otherwise returns value.

${ifEmpty(e.Department, "Unassigned")} → "Engineering" or "Unassigned"
${ifEmpty(e.Notes, "-")} → notes text or "-"

Simpler than coalesce when you have exactly one value and one fallback.

These functions operate on slices and are particularly useful with jx:updateCell for summary rows, or in any cell that needs to aggregate collection data.

Sums a numeric field across a slice of items.

${sumBy(employees, "Salary")} → 450000
${sumBy(orders, "Total")} → 12750.50

Calculates the average of a numeric field across a slice.

${avgBy(employees, "Salary")} → 75000
${avgBy(reviews, "Score")} → 4.2

Counts non-nil values of a field across a slice.

${countBy(employees, "Email")} → 15 (employees with email addresses)
${countBy(orders, "ShippedDate")} → 8 (orders that have shipped)

Returns the minimum value of a numeric field across a slice.

${minBy(employees, "Salary")} → 45000
${minBy(scores, "Value")} → 2.1

Returns the maximum value of a numeric field across a slice.

${maxBy(employees, "Salary")} → 120000
${maxBy(scores, "Value")} → 9.8

Walkthrough — totals row at the bottom of a loop

You’re rendering an employee table with jx:each(items="employees" var="e" lastCell="C5"). Right below the loop area (in row 6), you want a totals row.

CellValue
A6”Total”
B6${formatNumber(sumBy(employees, "Salary"), 0)}
C6${formatNumber(avgBy(employees, "Salary"), 0)}

sumBy and avgBy work on the full data slice (not just the loop iteration’s e), so they’re safe to use outside the loop too. Place them where you want them in the template — the engine evaluates them once at that position.

Creates a clickable hyperlink in the cell.

${hyperlink("https://example.com", "Visit Site")}
${hyperlink(e.ProfileURL, e.Name)}
${hyperlink("#Summary!A1", "Jump to summary")}
${hyperlink("mailto:" + e.Email, e.Name)}

The cell shows the display text and links to the URL. Works in Excel, Google Sheets, and LibreOffice.

Walkthrough — clickable email links per row

Each employee row should show the name as a clickable mailto link. In cell A2:

${hyperlink("mailto:" + e.Email, e.Name)}

When users click “Alice Johnson” in the output, Excel opens their mail client. Same idea works for ticket links (hyperlink("https://tickets.example.com/" + t.ID, "Ticket #" + t.ID)) or cross-sheet jumps (hyperlink("#Sheet2!A1", "Go to details")).

Adds a cell comment (note) to the cell.

${comment("Reviewed by finance team")}
${comment(e.Notes)}

The comment appears as a hover tooltip on the cell. Useful for adding context without cluttering the visible data.

Translates a key using the i18n translation map provided via WithI18n. If the key isn’t found, the key itself is returned (so you can spot missing translations in the output).

${t("header.name")} → "Name" (en) or "Nombre" (es) or "Name" (de)
${t("status.active")} → "Active" or "Activo" or "Aktiv"

Setup in Go:

translations := map[string]string{
"header.name": "Name",
"header.department": "Department",
"header.salary": "Salary",
"status.active": "Active",
"status.inactive": "Inactive",
}
xlfill.Fill("template.xlsx", "report.xlsx", data,
xlfill.WithI18n(translations),
)

Build one template, generate reports in any language by swapping the translation map. No separate templates per language.

Functions compose naturally in expressions:

${upper(ifEmpty(e.Department, "UNKNOWN"))}
${formatNumber(sumBy(items, "Total"), 2)}
${hyperlink(e.URL, title(e.Name))}
${join(", ", e.Skills) + " (" + formatNumber(len(e.Skills), 0) + " skills)"}

Need something not covered here? Register your own functions:

xlfill.Fill("template.xlsx", "report.xlsx", data,
xlfill.WithFunction("currency", func(args ...any) (any, error) {
amount := args[0].(float64)
code := args[1].(string)
return fmt.Sprintf("%s %.2f", code, amount), nil
}),
)

Use in templates: ${currency(e.Salary, "USD")} produces USD 75000.00.

See how all commands work together:

Commands Overview →

Or check the full API reference:

API Reference →