DEV Community

Cover image for I Built a Full IT Ticket System in Power Apps — Here's the SLA Engine That Runs Without Power Automate
Pankaj Jangid
Pankaj Jangid

Posted on

I Built a Full IT Ticket System in Power Apps — Here's the SLA Engine That Runs Without Power Automate

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)