Placeholders
A placeholder is a token like {{guest.name}} that gets resolved at task-fire time against the live reservation/guest/property/teammate row. They're how a single template can produce a personalised message for every guest.
Syntax
Simple lookup
{{guest.name}}
{{property.address}}
{{reservation.checkInDate}}
The path follows <entity>.<column>. The resolver does a generic SELECT * on the entity table and reads the named column.
Fallback chain
{{a|b|c}}
If a resolves to a truthy value, use a. Otherwise try b, then c. Useful for fields with overrides:
{{reservation.earlyCheckinTime|property.checkInTime}}
Says: use the reservation's earlyCheckinTime override if present; otherwise fall back to the property default.
Nested entity follow
The resolver auto-follows foreign keys. From a task you can write {{reservation.property.name}} or {{reservation.guest.nukiAccessCode}} — the resolver walks the FK chain.
Namespaces
| Namespace | Common fields | Notes |
|---|---|---|
guest |
name, email, phone, nukiAccessCode, twoNAccessCode, language, country |
Reads from the canonical guest row for the reservation. After #260 the picker prefers rows with populated access codes. |
reservation |
code, checkIn, checkOut, earlyCheckinTime, lateCheckoutTime, propertyName, totalPrice, nights, guestMood, guestCommunicationType |
One row per reservation. |
property |
name, address, checkInTime, checkOutTime, wifiPassword, parkingInstructions, guidebookText |
One row per property. |
teammate |
name, phone, language, noTranslateLanguages |
The teammate the task is assigned to. |
tenant |
name, defaultLanguage, whatsappPhoneNumberId |
Tenant-level config. |
The full schema is autodiscovered from the DB via SELECT *. If you can put a column in a SELECT against guests, you can write {{guest.<column>}}.
Validation
The editor validates that each placeholder resolves to a real column. After #254, the validator correctly recognises {{guest.email}} and {{guest.name}} (they were flagged as unrecognised before because the namespace list was hardcoded; it now reads from the live schema).
At fire time
When a task fires, the resolver:
- Loads the reservation row.
- For each placeholder in the action params, walks the path.
- Substitutes the value.
- If a placeholder doesn't resolve, the runtime decides:
- With a fallback chain — try the next.
- Without a fallback and
required=true— fail the task with a clear error. - Without a fallback andrequired=false— substitute empty string.
Examples
Hi {{guest.name}},
your access code for {{property.name}} is *{{guest.nukiAccessCode}}*.
You can check in any time after {{reservation.earlyCheckinTime|property.checkInTime}}.
The full address is:
{{property.address}}
WiFi: {{property.wifiPassword}}
resolves to:
Hi Johanna,
your access code for Castle&River Luxe-Vydrica is *847291*.
You can check in any time after 15:00.
The full address is:
Vydrica 12, Bratislava
WiFi: NocV12!
Related issues
- #254 —
{{guest.email}}/{{guest.name}}recognised by validator. - #185 —
{{property.id}}placeholder schema regression (Add FAQ entry). - #260 —
{{guest.nukiAccessCode}}resolves against canonical row.