I recently built a complete IT ticket management system in Power Apps —
9 screens, role-based access, live SLA tracking, and automatic email
notifications. The part I want to actually talk about here isn't the UI,
it's the SLA engine, because I built it to work without Power Automate,
and the trick is simpler than it looks.
The problem
SLA tracking normally means: a ticket is "Critical" → 60 minute target →
somebody needs to know if it's about to breach or already has. The
obvious way to do this is a scheduled Power Automate flow that checks
every ticket on a timer and flags the ones in trouble.
I wanted this app to run on Power Apps collections only — no flow, no
external data source — so a scheduled flow wasn't an option. The
question was: can you get "live" SLA status without a background job?
The trick: recalculate on every read, not on a timer
Instead of a flow updating a SLAStatus field periodically, I recalculate
it every time the app or a screen is opened, using Now() against the
stored due date:
\
UpdateIf(
colTickets,
Status <> "Resolved" && Status <> "Closed",
{
SLAStatus: If(
Now() > DueDate,
"Breached",
DateDiff(Now(), DueDate, TimeUnit.Minutes) <= SLAMinutes * 0.2,
"At Risk",
"On Track"
)
}
);
UpdateIf(colTickets, Status = "Resolved" || Status = "Closed", {SLAStatus: "Met"})
\\
This runs in App.OnStart, at the top of every screen's OnVisible, and
behind a manual "Refresh SLA" button. The At Risk threshold is 20% of
the SLA window remaining — so a Critical ticket (60 min target) goes
At Risk with 12 minutes left; a Low ticket (1440 min / 24 hrs) goes At
Risk with 4.8 hours left.
The honest tradeoff: this only updates when someone has the app open.
A ticket breaching at 2am with nobody looking won't trigger anything
until the next visit. For a real production deployment I'd pair this
with a scheduled flow for after-hours detection — but for a demo, an
internal tool with regular traffic, or anything where "eventually
consistent within the next visit" is fine, skipping the flow
entirely removes a moving part.
Role-based navigation, driven by data not code
The other pattern worth sharing: instead of hardcoding If(role = "Admin", ...)
checks scattered across the app, navigation is driven by a single table:
| Key | Title | AllowedRoles |
|---|---|---|
| dashboard | Dashboard | ,Admin,Manager,Assigned Person,User, |
| reports | Reports | ,Admin,Manager, |
| users | Users | ,Admin, |
The nav rail filters itself against the signed-in user's role:
\
Items: SortByColumns(
Filter(colNavigation,
!IsBlank(Find("," & varCurrentUser.Role & ",", AllowedRoles))),
"Sort"
)
\\
Adding a new role, or changing who sees what, is a data edit — not a
formula change on nine screens.
What else is in there
- Automatic Outlook emails on create/assign/status-change/resolve/close, all sharing one HTML template (nested tables, inline CSS — the only way HTML survives Outlook's rendering engine)
- PDF export of a ticket or the whole list, emailed directly from the app
- A 7-day ticket trend chart alongside the usual KPI tiles
Full build walkthrough (I go through every screen and formula) is here:
If you want the finished template instead of rebuilding it — SLA engine,
email templates, all 9 screens, plus migration guides to move it onto
SharePoint or Dataverse when you outgrow collections — I've packaged it
here: dialforit.com
Happy to answer questions on the SLA math or the Outlook HTML in the
comments — those were the two things that took the most iteration to
get right.

Top comments (0)