Workflow help
Workflows are automatic rules. You set them up once, and from then on the app handles the busywork — texting tenants, issuing door codes, sending reminders. No coding needed. This guide walks you through everything, with examples you can copy.
What's a workflow?
Think of a workflow as an "if this happens, then do that" rule. You decide what should trigger it and what should happen, and the app does the work for you every time.
A few real examples:
- A new tenant signs a lease. The app automatically creates their door code and texts it to them.
- Three hours before checkout, every guest gets a friendly reminder by text.
- When you delete a lease, the door code is automatically revoked — no more "did I remember to do that?"
Every workflow has a status. You'll see it in the workflow list:
- Live — turned on, doing its job.
- Paused — saved but turned off. Useful when you want to take a workflow offline temporarily without deleting it.
- Draft — what brand-new workflows start as. They won't actually run until you hit Publish. So if you build a workflow and it doesn't seem to do anything, this is the first thing to check.
The three parts
Every workflow has three pieces. You build them in this order in the workflow builder:
| Part | What it does |
|---|---|
| Trigger | The thing that starts the workflow — for example, "a lease was just created" or "every hour, check for upcoming check-ins." |
| Filters | Conditions that narrow it down. Without filters, the workflow runs every time the trigger happens. Add a filter to only run for, say, vacation rentals — or only for one specific property. |
| Steps | What you actually want to happen. Each step does one thing — send a text, issue a door code, etc. You can add as many steps as you want, and they run in order. |
Instant triggers
These run the moment something happens in the app. As soon as you (or someone on your team) creates, updates, or deletes one of these things, any matching workflow fires right away.
| What you do in the app | Triggers this |
|---|---|
| Create a lease | Lease created |
| Edit any field on a lease | Lease updated |
| Delete a lease | Lease deleted |
| Add a contact (including from the lease form) | Contact created |
| Edit a contact | Contact updated |
| Delete a contact | Contact deleted |
| Anything that creates an in-app notification (a chat message, a billing event, etc.) | Notification created |
Tip: The Notification created trigger fires for every in-app notification. Use a filter on type_name (for example, type_name eq chat_message) to react only to a specific kind.
The Schedule trigger
Some things happen because of time, not because someone clicked a button. "Send a reminder 3 hours before check-in." "Email me a summary every Monday." "Notify me when a lease ends today."
For all of those, use the Schedule trigger. The app checks once an hour — at the top of every hour — and looks for records that match your filters. If it finds any, your workflow runs once for each match. It also remembers which records it already handled, so you won't get duplicate reminders.
You can use Schedule on Lease or Contact records. So "find leases ending today" or "find contacts whose anniversary is this week" both work the same way.
How often it checks
Schedule triggers have a frequency setting. Here's what each one means:
| Setting | What it means |
|---|---|
hourly | Check every hour, on the hour. |
daily | Check at the next hourly check, but only once per day. After it runs, it waits at least 24 hours before running again. |
weekly | Same idea — runs at most once a week. |
monthly | Runs at most once a month. |
If you're not sure which to pick, hourly is usually the right answer — your filters control what actually fires anyway. Use the others when you specifically want a lower volume (like a daily summary email).
Conditions you can use
Filters narrow down when your workflow runs. Add as many as you want — they all have to be true for the workflow to fire.
| Condition | Needs a value? | Means |
|---|---|---|
equals | yes | Matches exactly |
not equals | yes | Doesn't match exactly |
contains | yes | The value contains the text you provided (capitalization doesn't matter) |
not contains | yes | The value does not contain the text |
is set | — | The field has any value (not blank) |
is empty | — | The field is blank |
is before now | — | The date is in the past |
is after now | — | The date is in the future |
within next | e.g. 3h, 1d, 2w, 1mo | The date is coming up within that amount of time |
within last | same | The date was within that amount of time ago |
For the within next and within last conditions, the value tells the app how far ahead or back to look:
3h— 3 hours2d— 2 days1w— 1 week1mo— 1 month (usemo, not justm, so the app knows you mean months and not minutes)
Filter examples
Here are some common goals and the filter that does it:
| What you want | Filter |
|---|---|
| Only run for one specific property | property equals "<the property's ID>" |
| Only run for leases that have already ended | end_date is before now |
| Send a checkout reminder ~3 hours before | end_date within next 3h |
| Skip leases without a phone number | contact_phone is set |
| Only fire for company emails | email contains "@company.com" |
Adding steps
Click + Add step in the workflow builder. Pick what kind of action you want (Smart Lock, Text Message, etc.), then pick the specific action.
Each step has its own form to fill in. The fields depend on the action — for a text message, you'll need a phone number and a body. For a smart-lock code, you'll need the lock and a date range. The next section explains placeholders, which let you fill those fields with information from the trigger automatically.
What each action does
Smart Lock · Issue access code
Creates a new door code on the lock and saves it in the app. Useful right after a tenant signs a lease.
| Field | Required? | Notes |
|---|---|---|
lock_id | yes | Which lock to add the code to. Find this on the lock's page in the app. |
property | yes | The property this is for. |
code | no | 4 to 9 digits. Leave it blank and the app picks a random 6-digit code for you. That's usually what you want. |
valid_from | yes | When the code starts working. Usually {{trigger.start_date}} from the lease. |
valid_to | yes | When the code stops working. |
lease_id | no | Connects the code to a lease so it shows up in the lease's record. |
Smart Lock · Update access code
Changes the start/end dates on the door code(s) for a lease. Use this if a lease's dates change and you want the code to stay in sync.
Smart Lock · Revoke access code
Removes the code from the lock and from the app. The tenant won't be able to use it anymore. This is usually the last step you'd add to a "lease deleted" workflow.
Text Message · Send text
Sends an SMS through your Twilio account. The phone number is auto-formatted, so a US number entered as 2146366019 will be sent as +12146366019. The body can include placeholders (see below).
Notification · Send notification
Creates an in-app notification — the kind that shows up on the bell icon at the top of the app. Useful for nudging yourself or your team about something important without sending a text.
| Field | Required? | Notes |
|---|---|---|
type_name | yes | One of: chat_message, lease_starting_soon, lease_ending_soon, payment_received, payment_failed, workflow_completed, workflow_failed, system_announcement. The type controls the icon and default color. |
title | yes | The headline shown on the notification (one line). Placeholders work — e.g. Lease ending for {{trigger.contact_first_name}}. |
body | no | Longer description shown under the title. Placeholders work. |
severity | no | One of info, success, warn, error. Overrides the type's default color (e.g. mark a payment failure red). |
user_id | no | Send to one specific team member. Leave it blank to send to everyone on your account. That's the usual choice. |
entity_type / entity_id | no | Link the notification to something in the app — for example, entity_type = lease and entity_id = {{trigger.lease_id}}. Used so the bell can clear the badge when you open the related screen. |
dedupe_key | no | Prevents duplicate notifications. If you fire the same workflow many times in a row, set this to something stable (like the lease id) and only one notification is created until it's read. |
Placeholders
When your workflow runs, you'll usually want to pull in info from whatever triggered it. The tenant's name. The lease's start date. The phone number on file. Instead of typing all that out by hand, you use placeholders.
A placeholder looks like this:
{{trigger.contact_first_name}}
The double curly braces tell the app "replace this with real data when the workflow runs." So if your text body is:
Hi {{trigger.contact_first_name}}, welcome to your stay!
And the workflow fires for a tenant named Sarah, the actual text she gets is:
Hi Sarah, welcome to your stay!
The two main kinds of placeholders are:
| Placeholder | What it pulls in |
|---|---|
{{trigger.something}} | A piece of info from whatever started the workflow — the lease, the contact, etc. |
{{steps.s1.something}} | A piece of info from a previous step. Step 1 is steps.s1, step 2 is steps.s2, and so on. |
For example, after step 1 generates a door code, step 2 (the text message) can include it like this:
Your door code is {{steps.s1.code}}.
You don't have to memorize the placeholder names. When you're editing a step, the right-hand panel of the builder shows every placeholder that's available, with one-click insert. Just click into the field you want to fill, then click the placeholder. Done.
Functions
Sometimes you don't just want to drop in a value as-is — you want to change it a little. Format a date so it reads "April 29, 2026" instead of the raw 2026-04-29T16:00:00.000Z. Make a name ALL CAPS for a header. Round a price to 2 decimals.
That's what functions are for. Each one belongs to a category — string (for text), number, or date — and you call it like this:
{{date.format(trigger.start_date, 'MMMM D, YYYY')}}
That formats the lease's start date as "April 29, 2026". The first thing inside the parentheses is what to format; the second is how you want it formatted (see date formats below).
You'll see all the available functions in the right-hand panel of the workflow builder, grouped by category. Click any one to insert it into the field you're editing.
Here's the full list:
Date formats
When you use date.format, you tell it how you want the date to look using letters as placeholders. Mix and match these to get exactly the format you want — anything that isn't a placeholder (like dashes, spaces, or commas) shows up as-is.
| Letters | What they show | Example |
|---|---|---|
YYYY or YY | year | 2026 or 26 |
MM or M | month as a number | 04 or 4 |
MMMM or MMM | month name | April or Apr |
DD or D | day of month | 29 |
dddd or ddd | weekday | Wednesday or Wed |
HH or H | hour (24-hour) | 16 |
mm | minute | 00 |
ss | second | 00 |
Some examples:
'YYYY-MM-DD'→2026-04-29'DD-MM-YYYY HH:mm'→29-04-2026 16:00'dddd, MMMM D'→Wednesday, April 29'MMM D, YYYY'→Apr 29, 2026
One thing to know about times. Dates are always shown in UTC time, not your local time zone. So if a lease is set to start at 4:00 PM UTC, the formatted date will read 16:00 — even if you're in Eastern Time, where that's actually noon. If your dates look "off by a few hours," this is usually why. Account-timezone support is on the roadmap.
Combining functions
Functions can be plugged into each other. You only need one outer pair of {{ }} — the rest goes inside.
For example, "format the day after the lease starts":
{{date.format(date.addDays(trigger.start_date, 1), 'DD-MM-YYYY')}}
Read it inside-out: first date.addDays(...) takes the start date and adds 1 day. Then date.format(...) takes that result and formats it. You can keep nesting like this as much as you need.
A few more examples:
{{string.upper(string.concat(trigger.contact_first_name, ' ', trigger.contact_last_name))}}
→ "SARAH JOHNSON"
{{number.round(number.divide(steps.s1.total, steps.s1.count), 2)}}
→ "12.34"
{{string.concat('Hi ', trigger.contact_first_name, ', code: ', steps.s1.code)}}
→ "Hi Sarah, code: 482910"
Checking what ran
Every time a workflow fires, it leaves a record. There are a few places to look:
- Workflow info panel — when you open a workflow, the right side shows how many times it's run total and how many of those failed.
- Click "Runs: N" to see the list of every run for this workflow. You can filter by All, Success, Failed, or Skipped at the top.
- Click "Failed: N" to jump straight to just the failures.
- "Total runs" tile on the workflow list page — opens an activity log across all your workflows. Handy for "what's been happening today?"
Each run shows its status, what action ran (or what failed), when it happened, and how long it took. Click "Trigger payload + step results" on a row to expand the details — useful if you're investigating why something didn't quite do what you expected.
Notice the ↻ rerun button on each row. It re-runs that specific entry with its original data. Helpful for "I fixed the typo, now retry that failed text" or "send that confirmation again, the tenant didn't get it."
Don't need old failures cluttering the view? Open the runs modal, switch to the Failed tab, and click Clear failed. The records get cleaned up. (Your central error log keeps a copy either way.)
When something doesn't work
You'll occasionally see runs marked as failed. Here are the most common reasons and what to check:
| What you'll see | What it means | What to try |
|---|---|---|
Action 'Send text' requires 'to' mapping | You forgot to fill in the phone number field. | Open the step, fill in the to field with {{trigger.contact_phone}} or a static number. |
'From' +1... is not a Twilio phone number | Your Twilio settings aren't quite right. | Check that TWILIO_FROM_NUMBER in your config matches a number you actually own in Twilio. |
Invalid date | One of the date functions got something it couldn't read. | Make sure the field you're using actually has a date in it. Check the live preview in the step editor. |
Unknown function: date.formaat | Typo in a function name. | Check spelling, or use the function picker on the right side of the step editor instead of typing. |
Lock not found | The lock ID you mapped doesn't exist or isn't on your account. | Re-copy the lock ID from the lock's page. |
The live preview is your friend. When you're editing a step, the field you're typing in shows a small preview underneath of what the value will look like when the workflow actually runs. If you see ⚠ red text, something's wrong — fix it before saving and you'll save yourself a failed run.
Example: onboard a tenant
Goal: When a new lease is created, automatically issue a door code and text it to the tenant.
| Setting | Value |
|---|---|
| Trigger | Lease / Lease created |
| Filters | (none — fire for every new lease) |
| Step 1 | Smart Lock / Issue access code |
lock_id | (the smart lock's ID — find it on the lock's page) |
property | {{trigger.property}} |
valid_from | {{trigger.start_date}} |
valid_to | {{trigger.end_date}} |
lease_id | {{trigger.lease_id}} |
code | (leave blank — the app generates one) |
| Step 2 | Text Message / Send text |
to | {{trigger.contact_phone}} |
body | Hi {{trigger.contact_first_name}}, your code is {{steps.s1.code}}. Valid {{date.format(trigger.start_date, 'MMM D')}} – {{date.format(trigger.end_date, 'MMM D')}}. |
The text the tenant receives will look like: "Hi Sarah, your code is 482910. Valid Apr 29 – May 3."
Example: pre-arrival reminder, ~4 hours before check-in
Goal: A friendly text to upcoming guests about 4 hours before their check-in time.
| Setting | Value |
|---|---|
| Trigger | Lease / Schedule |
| Frequency | hourly |
| Filter | start_date within next 4h |
| Step | Text Message / Send text |
to | {{trigger.contact_phone}} |
body | Hi {{trigger.contact_first_name}}, looking forward to your arrival at {{date.format(trigger.start_date, 'h:mma')}}! |
Each guest gets the message exactly once — even though the app checks every hour, it remembers who it already texted.
Example: when a lease is deleted
Goal: When a lease is deleted, automatically revoke any door codes that were tied to it.
| Setting | Value |
|---|---|
| Trigger | Lease / Lease deleted |
| Filter | (none) |
| Step | Smart Lock / Revoke access code |
lease_id | {{trigger.lease_id}} |
The app remembers which codes were tied to that lease before the delete happens, so the revoke action can find them and clean them up. No extra config needed — it just works.
URManager workflow help. Last reviewed 2026-04-29.