October 29, 2026

#token# Substitution in Yardi Custom Reports, Explained

Open any Yardi Voyager custom report script and you'll find text like #p.hMy# or #asofdt# sitting inside a SQL statement, wrapped in hash marks. That's a token. It isn't a variable in the SQL-engine sense — Voyager's report runner does a textual substitution pass before the query ever reaches the database, replacing each #Name# with whatever value the user supplied at run time, then hands the resulting SQL text to the server.

That distinction — textual substitution before execution, not a bound parameter at execution — is the thing to understand before writing a single //Filter row, because it explains both why tokens are so flexible and why an unfilled one can break a query in a way a bound parameter never would.

Where a token's value comes from

Every runtime prompt on a report is declared as a //Filter row: a comma-delimited definition with fields for the filter's type, data type, name, caption, and value template. The Name field is the token stem — it's what turns into #Name# wherever that text appears in the report's SQL. A range filter pairs two stems as from:to, so a single filter row can generate two independent tokens.

The filter's Val1 field is the actual SQL fragment that gets injected into the query's condition slot once the filter is supplied — and it's the piece that contains the #Name# reference. Multi-select filter templates pre-quote the token, something like col in ('#Name#'), so that when Voyager expands multiple selected values it produces a valid IN list rather than a single quoted string with commas jammed inside it.

Two other places tokens come from

Not every token traces back to a //Filter row a user fills in.

Record-id tokens. Document and single-record reports — an invoice, a lease abstract, a statement of account — are driven by a generic driving-record id, #hRecord#, passed in by whatever screen or scheduler invoked the report. Every dataset in that report is keyed WHERE <pk> = #hRecord#, so the same token drives every dataset in a multi-dataset document without each one needing its own filter.

System tokens. Voyager also populates a family of #@@NAME# tokens server-side, independent of any filter the user fills in: #@@USERNAME# (the current Voyager user, useful for locale-mask lookups), #@@userID#, #@@GUID#, #@@WEBSHARENAME# (the webshare base URL, for building deep links inside a report), and #@@EMAILREPORTS# — which resolves to 'True' only when the report is actually running as an outbound scheduled email. That last one is the standard gate for audit writes that should only fire when the report is genuinely being emailed out, not previewed on screen.

Date and period filters get their own token family too — #begmonth#/#endmonth#, #frommonth#, #asofdt#/#asofdate#, #dt1#/#dt2# or #dtfrom#/#dtto# for range pairs, #Period#, #Year#. Because these substitute as plain strings, the convention is to always wrap them in quotes inside the SQL — '#asofdt#' — and let the filter's data type do the work of validating the input format rather than the SQL itself.

The substitution is textual — so an empty token is still valid SQL

Because the substitution happens before the query runs, an unfilled token doesn't cause a parameter-binding error the way a missing bound value would in most application code. It substitutes to an empty string — or, if the filter is inactive entirely, the literal text #name# is left in the query unchanged. Either outcome is syntactically legal SQL text once substitution finishes; whether it's correct SQL depends entirely on how the query around it was written.

That's why an optional filter needs an explicit guard, not an assumption that Voyager will handle the blank case for you. The standard idiom tests for both outcomes by checking the first character of the substituted text:

CASE WHEN '#tok#' = '' THEN <all-rows expr>
     WHEN CHARINDEX('#', '#tok#') = 1 THEN <all-rows expr>
     ELSE <filtered expr>
END

The first branch catches a token that substituted to nothing. The second catches a token that was never substituted at all — still carrying its literal # characters — which happens when the filter itself wasn't active for this run. A free-text "notes contains" filter is the classic use case: wrapped this way, the query returns every row when the filter is blank or inactive, and only the matching rows once someone actually types something into the prompt.

Skip the guard and the failure mode isn't a clean error. It's a WHERE clause built from a literal #namelike# string, or an empty-string comparison that quietly returns nothing — a report that looks like it ran fine and returned zero rows, or a report that ran fine and returned every row when the user expected a filtered subset. Neither failure explains itself.

Why this matters beyond one report

Report scripts get no validation until they actually run — there's no compile step catching a malformed token reference before a scheduled job fires it at 2am. That makes the token grammar one of the places where getting the mechanics right the first time matters more than in ordinary application SQL, where a bad bind parameter usually just throws.

It also means the guard idiom above is worth keeping close at hand rather than re-deriving it from scratch on every report that has an optional filter — which is most of them.

Draft reports with the grammar already loaded

The SQL Query Assistant carries this token grammar as part of its report-writing knowledge base, alongside the YSR/SSRS script format, the //Columns and //Filter row grammars, and the render engines — including FillDocs Word and Excel templates, which bind cells to dataset fields the same way filters bind SQL fragments to tokens. Ask it to draft a report with an optional free-text filter and it applies the guard idiom by default, instead of leaving you to discover the empty-token trap the first time a scheduled run comes back wrong.

It ships in every PropETL tier. Start the free 7-day trial — no credit card — and hand it your next report's filter list.

Featured on FoundrListFeatured & Verified on TrustMRRPropETL on PeerPushLaunchpadly Startup DirectoryPropETL — Featured on LaunchBuck