What’s on our mind: Deep diving into Koha notice templates with Template Toolkit

Last updated on: 5th May 2026| 16th April 2026 | Martin Renvoize | Koha

Koha’s notice system is incredibly powerful, but it can also be a black box. While the default notices work out of the box, customising them often feels like navigating a maze blindfolded.
If you’ve ever stared at a notice asking, “How do I get the patron’s home branch name instead of the code?” or “Why does this loop work in the checkout receipt but not the overdue notice?”, this post is for you.
We are going to go beyond the basics. We are going to look at how to use Template Toolkit (TT) syntax, but more importantly, we are going to learn how to “code dive” to find the hidden variables available to your notices.

Part 1: The basics (A quick refresher)

Before we break things, let’s review the syntax. Koha uses Template Toolkit (TT) for notices. This allows for logic, loops, and data processing directly in the email or slip.

Printing variables

To display data, wrap the variable name in square brackets and percent signs.

Hello [% borrower.firstname %],

Conditional logic

Don’t send a “Please call us” line if the library doesn’t have a phone number. Use IF statements.

[% IF branch.branchphone %]
    Give us a call at [% branch.branchphone %]
[% END %]

Filters

TT can format data for you. Need to capitalise a name or format a price?

[% borrower.surname | upper %]  [% amount | $Price %]

Some useful Template Toolkit filters

Template Toolkit includes many powerful filters that can transform data right within your notice template. Here are a few core filters highly useful for Koha notices:

upper, lower
Purpose: Converts text to all uppercase or all lowercase.

Example usage:

[% borrower.surname | upper %]

Output example: SMITH

Context: Name field

trim
Purpose: Removes leading and trailing whitespace from a string.

Example usage:

[% item.description | trim %]

Output example: “A Great Book”

Context: Item description or title

html
Purpose: Escapes special characters into HTML entities for security and correct display.

Example usage:

[% item.title | html %]

Output example: <Escaped Title>

Context: Any text that might contain HTML/XML

url
Purpose: URL-encodes a string (e.g., spaces become %20).

Example usage:

[% item.title | url %]

Output example: Title%20with%20spaces

Context: Creating URL query parameters

default
Purpose: Provides a fallback value if the variable is undefined or empty.

Example usage:

[% item.notes | default('No notes provided') %]

Output example: No notes provided

Context: Optional fields

remove
Purpose: Removes all occurrences of a specified substring.

Example usage:

[% item.publisher | remove('Publishing Co.') %]

Output example: McGraw-Hill

Context: Standardising publisher names

replace
Purpose: Replaces occurrences of one substring with another.

Example usage:

[% item.location | replace('STACK', 'Main Floor') %]

Output example: Main Floor 100-200

Context: Reformatting cryptic location codes

Note on date/time filters: While TT has its own date filters, Koha often provides its own specialised date filters (like $KohaDates or $KohaTime) specifically configured to respect system preferences.

 

Part 2: The deep dive (Finding the hidden data)

This is the most common frustration: “What variables can I actually use?”

The Koha wiki has some lists, but they are often incomplete. The only source of truth is the code itself.

Step 1: Identify the notice code

Go to Tools > Notices and Slips. Find the notice you are editing and grab its Code.

  • Example: Let’s look at the CHECKOUT (Checkout) notice.

Step 2: Search the codebase

Go to the Koha Community Git Browser. You are looking for the Perl script that triggers this notice.

The magic function you are searching for is GetPreparedLetter.

  • Search query: GetPreparedLetter (or your notice code like 'ODUE').
  • Where to look:
    • For circulation notices: C4/Circulation.pm or circulation.pl.
    • For overdues: misc/cronjobs/overdue_notices.pl.
    • For holds: C4/Reserves.pm.

    Step 3: Analyse the tables and substitute

    Once you find the relevant call to GetPreparedLetter, look at the arguments passed to it. It usually looks something like this:

    my $letter = C4::Letters::GetPreparedLetter(
        module      => 'reserves',
        letter_code => 'HOLDPLACED',
        branchcode  => $branch,
        lang        => $patron->lang,
        tables      => {
            'branches'    => $library->unblessed,
            'borrowers'   => $patron->unblessed,
            'biblio'      => $biblionumber,
            'biblioitems' => $biblionumber,
            'items'       => $checkitem,
            'reserves'    => $hold->unblessed,
        },
        substitute    => {
            today => output_pref(dt_from_string) 
        }
    )

    Here is your treasure map:

      1. tables: These are database tables. If you see 'borrowers' => $borrower, it means the system is fetching a row from the borrowers table.
        • Translation: You can usually access any column in that table using [% borrower.column_name %].

     

    1. substitute: These are custom variables or arrays.
      • Translation: If you see 'today', you can use [% today %].

     

    Step 4: The C4::Letter variable mapping

    How does the Perl object $borrower become [% borrower.firstname %]?

    If you look at C4::Letter.pm, specifically the _get_tt_params function, you will see how Koha maps these. It iterates over the keys in the tables hash.

    • It takes the table name (e.g., borrowers) and uses it as a prefix.
    • It takes the column name (e.g., surname) and appends it.
    • Result: [% borrowers.surname %] (Note: notice syntax sometimes aliases this to the singular borrower depending on context, which is why testing is key).

 

Part 3: Looping through relations

The most powerful part of TT is looping through related data, like a list of overdue books.

If you saw an array reference in the code (like \@items in the example above), you can loop over it.

<h3>You have the following items checked out:</h3>
<ul>
[% FOREACH item IN items %]
    <li>
        <b>[% item.title %]</b>
        (Due: [% item.date_due | $KohaDates %])
        <br/>
        Barcode: [% item.barcode %]
    </li>
[% END %]
</ul>

The “object” advantage

In newer Koha versions, developers are passing Koha::Objects rather than simple hashrefs. This is a game changer.

If the code passes a Koha::Patron object as borrower, you aren’t limited to just the columns in the borrowers table. You can potentially chain relation method calls if the template context allows it.

  • Example: [% borrower.category.description %]
  • Here, we are jumping from the Patron object to the related Category object to get the description, all without writing a complex SQL join.

 

Part 4: Why is this so hard? (And a proposal)

You might be thinking, “Why do I have to read Perl code just to write an email?”

It is a fair question. The difficulty lies in Context. A generic “list of variables” is impossible because the CHECKOUT notice has access to items, but the PASSWORD_RESET notice does not. The data is prepared dynamically at the moment the event happens.

The dream: No more code diving

Imagine a system where developers explicitly declare the “Data Contract” for every notice.

  • Developer side: When writing the Perl code, they define: “This notice provides a Patron Object and a list of Item Objects.”
  • User side: The Notice Editor reads this contract and gives you a sidebar with a tree-view of available variables. You click “Patron > First Name” and it inserts [% borrower.firstname %].

 

Call to action

This level of transparency requires refactoring how Koha handles data passing in notices. It’s not as simple as a UI update; it requires backend plumbing to standardise how data is exposed to templates.

However, the payoff would be massive: simpler customisation, fewer bugs, and no more reading C4::Letters.pm on a Friday afternoon.

Is this something that would save your library time? Features like this often come to life through community sponsorship. If you want to make notice writing easier for everyone, consider raising this on the mailing list or sponsoring a developer to tackle the “Notice Data Contract” project!


 

Previous

What’s on our mind: Validating ISBNs in Koha 25.11

Overturned silver fern with more ferns in the background
Next

Open Fifth Koha & Aspen Discovery Development Updates – April 2026

Photograph of bookshelves