Every posted transaction in Yardi Voyager lands in one table: TRANS. What kind of transaction it is — a tenant charge, a vendor payment, a bank transfer, a journal entry — is not a separate table, a separate module, or a readable label. It's a single integer column, iType, and if you decode it wrong your query still runs. It just returns the wrong answer, silently, forever.
That's the landmine with every integer-coded discriminator field in Voyager: iType = 7 compiles fine whether or not you know that 7 means "AR charge." Nothing throws an error when you filter for iType = 6 thinking it's a deposit instead of a tenant receipt. The rent roll runs. The number is just wrong, and it stays wrong until someone reconciles it against the general ledger and can't figure out why.
Why this specific field is worse than most
Most Voyager status fields at least have a matching decoder view somewhere in the schema — tenstatus for TENANT.iStatus, for instance. TRANS.iType doesn't hand you that convenience. It's a bare integer with no companion lookup table shipped in the standard schema, which means the decode has to come from somewhere else: either institutional memory, or execution-verified testing against a live database.
That's also why it's a landmine and not just an inconvenience. TRANS sits at the center of Voyager's financial spine — the SQL Query Assistant's schema knowledge describes it as the header table that DETAIL (line items) and GLDETAIL (ledger postings) both hang off. Get iType wrong at the header and the error propagates through every join built on top of it: AR aging, AP aging, GL activity, delinquency reports. One misread code, many wrong reports.
The complete map
PropETL's schema knowledge base carries a decoded, execution-verified map of TRANS.iType — the same one referenced on the SQL Query Assistant page as "the complete 14-code TRANS.iType map." Fourteen distinct values have been observed on live Voyager transaction data. Eight of them carry confirmed semantic labels:
- 2 — AP payment / check
- 3 — AP invoice (vendor payable)
- 5 — Bank deposit
- 6 — Tenant receipt
- 7 — AR charge
- 10 — Journal / non-party posting
- 13 — System / batch control record — exclude from financial reports
- 18 — Bank withdrawal / transfer
The remaining six observed values (9, 11, 12, 15, 16, 19) are lower-volume codes that have been profiled against live data but don't yet have confirmed semantics beyond "not an AP invoice." That distinction matters: a decode that's honest about what it doesn't know is more useful than one that guesses a plausible-sounding label and gets baked into a report. If you're drafting ySQL against TRANS.iType and land on one of those six, the correct move is to filter by the observed value and hold off on labeling it — not invent a name for it.
The trap hiding one level down: HPERSON is polymorphic
Getting iType right isn't the end of it. TRANS.HPERSON — the column that tells you who the transaction belongs to — changes meaning depending on which iType you're looking at:
- On tenant receipts (6) and AR charges (7),
HPERSONpoints to a tenant. - On AP invoices (3), it points to a vendor.
- On bank deposits (5) and withdrawals (18), it points to a bank record instead of a person or company at all.
- On journal postings (10) and batch-control rows (13), it's zero — no party.
- On AP payments (2),
HPERSONis not the vendor. To find the vendor on a payment row, you have to follow the payment through to the invoice it cleared and read the vendor off that invoice instead.
That last one is the classic version of a trap that shows up all over Voyager's schema: a foreign-key-shaped column whose actual target table depends on a sibling discriminator column, not on the column name. (We've written up the general form of this — the HMYPERSON subtype trap — separately, because it isn't unique to TRANS.)
There's a second, quieter trap in the bank-type rows. iType = 5 and iType = 18 transactions carry NULL in both HPROP and the post-date column — which means any query that joins through property or filters by month will silently drop them. They aren't missing from the database; they're just invisible to the join pattern most reports use by default.
What this looks like as a query
A correctly scoped charge query filters iType = 7 explicitly rather than assuming every TRANS row is a charge:
SELECT p.sCode, t.cAmount, t.dtDate
FROM TRANS t
JOIN PROPERTY p ON p.hMy = t.hProp
WHERE p.sCode = '{PROPERTY_CODE}'
AND t.iType = 7 -- AR charge
AND t.dtDate >= '{START_DATE}'
The comment isn't decoration. Without it, the next person to touch this query — including a future version of you — has no way to know why 7 is the right number, or what happens if it silently becomes 6.
Stop keeping this in your head
The traditional fix for this is a private cheat sheet, usually incomplete, usually undocumented, usually lost when the person who built it changes jobs. The SQL Query Assistant ships the decode as queryable knowledge instead — 228 install-invariant decode rows across 32 decoders, including the full TRANS.iType map, so you can ask "what does iType = 13 mean and should I include it in this report?" and get the verified answer along with the trap it's attached to, right inside Claude.
It's included in every PropETL tier. Start the free 7-day trial — no credit card — and ask it to decode the next magic number your report turns up.
