Inbox Triage on Microsoft 365
One thread is one item, and Outlook already gives you the key that makes that true. The rest is a Choice column with fill-in switched off and a flow that has no delete action in it.
Used by
- AI Officer
- Business Systems Analyst
- Office Administrator
How to get this live in your business
The shared mailbox is the one place in this stack where Microsoft has already done the hard part. Every message arriving in a shared mailbox carries a conversation id, which is exactly the key the standard needs for one thread, one item. What Power Automate will let you do is raise a second item anyway, acknowledge a complaint on the way past, and delete something. None of those three is a capability problem. Each is a branch in the wrong place.
In short
What this is
A build plan for the inbox triage standard on Microsoft 365. A shared mailbox, six lists, three libraries, four flows and seven measures.
The core rule
The conversation id off the mail trigger is the key, held by an enforced unique value. A follow-up appends to the item it finds. It never raises a second one.
What it does not do
No flow in this build has a delete action, for any kind, including spam. Nothing with a price, a date or a decision in it leaves without an approval that names the person who released it.
The hard part
Two things the standard asks for that this stack does not hand you: a content hash on the filed attachment, and sight of the mail Exchange already moved to Junk. Both are called out below rather than papered over.
Take the spec to Claude, ChatGPT or Copilot. The sentence to paste is one click away.
Want the whole thing by email?
This paper, plus the rest of the library. One click to stop them.
1. Before you start
Read the inbox triage build standard first. It defines its objects, and the lists below are those objects with SharePoint types on them. Shared conventions are in building the standards on Microsoft 365.
Decide one thing first: what a message gets filed against. The standard is unambiguous that filing means a record id, never a copy in a folder, so you need to know what ids exist before you design anything. A job system with an API gives you job ids. Xero gives you supplier ids. The standard names Tradify as the case where the job side cannot be met at all, and says the honest route there is supplier filing with the job reference kept on the item. Pick your id space now, because RecordRef below is the column the whole standard turns on and it is the one that cannot be retrofitted.
Two things this stack does not give you, both named in the traps. There is no action in Power Automate that returns a content hash of a file, so the standard’s hash on the filed attachment and on the quarantined message has no source in the box. And the mail trigger sees the folder you point it at, which means anything Exchange has already filed to Junk is invisible to a build that claims it never deletes a message. Read both before you start, not after the bookkeeper asks where last month’s remittance went.
2. The lists, with their columns
For your developer. Show the fields
Site: Inbox
LIST Items (one per thread, and only one)
ThreadKey Text indexed, UNIQUE. The conversation id off the mail trigger.
Kind Choice supplier_invoice | quote_request
| job_update | statement | complaint | remittance | spam | other
FILL-IN CHOICES OFF. The list is closed.
RecordRef Text indexed. job:<id> or supplier:<id>. Blank only where no record matches, or Kind is spam or other.
Owner Person REQUIRED. Copied from Queues.
Due DateTime RaisedAt plus the kind's clock
Status Choice open | answered | escalated | quarantined
RaisedAt DateTime
EscalatedTo Person blank until the clock runs out
LIST Mail (one per message, append only)
MessageId Text indexed, UNIQUE
ThreadKey Text indexed
From Text
Subject Text
ReceivedAt DateTime
BodyRef Hyperlink into Bodies
FiledTo Text the record id. Never a folder path.
LIST Queues (the table a person owns)
Kind Choice indexed, UNIQUE. supplier_invoice
| quote_request | job_update | statement | complaint | remittance | spam | other
One row per kind.
Owner Person
Manager Person who it escalates to at the clock
ClockHours Number a complaint's is hours, a statement's a week
LIST Acknowledgements (the only automatic outbound)
ThreadKey Text indexed, UNIQUE
SentAt DateTime
PromisedBy DateTime the item's Due, and nothing else. No free-text column exists here on purpose.
LIST Releases (a named person, never a rule)
ThreadKey Text indexed
Person Person REQUIRED
ReleasedAt DateTime
WordsRef Hyperlink the exact text approved, into Sent
LIST EvidenceIndex (the attachment, not the body)
MessageId Text indexed
RecordRef Text indexed. supplier:<id> or job:<id>.
FileRef Hyperlink into Attachments
FileName Text
Fingerprint Text see the traps. Not a hash unless you added the step that makes one.
LIBRARY Attachments the artefact. Queryable, countable.
LIBRARY Bodies the raw message, kept
LIBRARY Quarantine spam. Retrievable. Nothing is deleted.EvidenceIndex is a list pointing at a library rather than an attachment column on Items, for the reason the Invoice Check build needs: an attachment cannot be queried and cannot be counted, and the whole downstream job is finding the supplier invoice where it was filed. Acknowledgements has no body column, which is the cheapest possible way to guarantee the automatic outbound cannot quote a price.
3. Every rule, and what enforces it
| The rule | Enforced by | How |
|---|---|---|
| Every message gets exactly one kind from a closed list | Kind, a Choice column | Eight values, single select, allow fill-in choices turned off. Whatever classifies the mail sits outside this plan; what SharePoint enforces is that it cannot invent a ninth kind, and a kind it cannot decide lands in other. |
| A message about a job or a supplier is filed against that record by id | Mail.FiledTo | The value is the record's own id from the job system or Xero. The flow has no move-to-folder step for a filed message, because a folder holds copies and a copy is what gets paid twice. |
| A supplier invoice attachment is filed as evidence with the message id and the supplier record | Attachments + EvidenceIndex | The file and the index row are written in the same run, so the Invoice Check build reads it where it was filed. The hash the standard also asks for has no source in this stack, which is a trap below rather than a column that lies. |
| Every classified message lands in the queue for its kind, with a named owner and a due date | Queues list | Owner and Due are copied onto the item from the Queues row for its kind. A missing Queues row stops the raise rather than producing an item nobody owns, which is the whole reason the clock is a list and not an expression. |
| A complaint is escalated to a named person with the full message, and nothing is sent to the customer | Flow order | The complaint branch terminates the flow before the acknowledgement step exists in the run. It is not a condition sitting beside the send, because a condition beside a send is one edit away from being bypassed. |
| Any substantive reply carries a named person's release | Approval + Releases | The reply is sent inside the same flow as the approval, from the words stored on the Releases row, not from the words in the request. One flow in the site has a send action against the mailbox, and it is this one. |
| A follow-up in the same thread updates the existing item | Items.ThreadKey, unique | The conversation id off the trigger is the key. A create that loses a race fails on the unique value, and the failure branch is the one that appends the message id to the item that already exists. |
| Spam is quarantined, never deleted | Quarantine library | The message is copied to the library and moved to a folder. No flow in this build contains a delete action, which is a thing you can verify by reading them rather than by trusting them. |
4. The four flows
For your developer. Show the fields
FLOW 1 Land
trigger a new email arrives in the shared mailbox
logic save the raw message to Bodies, create the Mail row
look for an Items row with this conversation id
found -> append MessageId, file to the same record, STOP
classify, then create the Item
create failed on ThreadKey -> another run won. Append, STOP.
Kind = spam -> copy to Quarantine, move the mail,
Status = quarantined, STOP
Kind = complaint -> escalate to the Queues manager whole,
Status = escalated, STOP.
No acknowledgement. Not even the harmless one.
otherwise -> Owner and Due from Queues
file to the record by id
attachments to Attachments + EvidenceIndex
one Acknowledgement row, then send it
never a delete action. This flow does not contain one, for any kind.
FLOW 2 Reply, released
trigger an operator action on an Item
logic refuse outright where Kind = complaint
Start and wait for an approval from the named person
rejected -> nothing is sent, and the item stays open
approved -> write the Releases row with the responder and
the approved words
-> send exactly those words, read back off the row
-> Status = answered
never send and record after. And never compose from the request
rather than from the release.
FLOW 3 The clock
trigger scheduled, hourly
logic Items where Status = open, Due has passed, EscalatedTo blank
write EscalatedTo from the Queues row's Manager
post to that person with the item and its age
never raise a second item, or move Due. The clock belongs to the
kind, and an item does not get to quietly get older.
FLOW 4 Quarantine review
trigger scheduled, monthly
logic post the month's quarantined items to the Quarantine owner
with a link to each file in the library
never empty the library. The day the classifier calls a real
remittance spam is the day this matters.The order inside Flow 1 is the design. Spam and complaint terminate before the acknowledgement step is ever reached, so a cheerful automatic reply on a complaint is not something a misconfigured condition can produce. Everything else in that flow is recoverable; that one is not.
5. The Power BI model and its measures
For your developer. Show the fields
Acknowledged Complaints = -- must be zero, every day
VAR Acked = VALUES ( Acknowledgements[ThreadKey] )
RETURN
COUNTROWS (
FILTER (
Items,
Items[Kind] = "complaint" && Items[ThreadKey] IN Acked
)
)
Unowned Items = -- must be zero
CALCULATE ( COUNTROWS ( Items ), ISBLANK ( Items[Owner] ) )
Unfiled = -- no record id, and not "other"
CALCULATE (
COUNTROWS ( Items ),
ISBLANK ( Items[RecordRef] ),
Items[Kind] <> "other"
)
Open Past Clock =
CALCULATE (
COUNTROWS ( Items ),
Items[Status] = "open",
Items[Due] < NOW ()
)
Other Share = -- where the classifier gave up
DIVIDE (
CALCULATE ( COUNTROWS ( Items ), Items[Kind] = "other" ),
COUNTROWS ( Items )
)
Invoices Without Evidence =
VAR Filed = VALUES ( EvidenceIndex[MessageId] )
RETURN
COUNTROWS (
FILTER (
Mail,
RELATED ( Items[Kind] ) = "supplier_invoice"
&& NOT ( Mail[MessageId] IN Filed )
)
)
Release Latency Hours =
AVERAGEX (
Releases,
DATEDIFF (
RELATED ( Items[RaisedAt] ),
Releases[ReleasedAt],
HOUR
)
)Acknowledged Complaints is the canary and it belongs on the front page even though it should read zero forever. A measure that is always zero is worth keeping precisely when the day it is not zero is the day somebody sent a cheerful automatic reply to a customer who was already angry.
6. The order to build it in
- Settle the id space: which system gives you job ids, which gives you supplier ids, and what happens to a message that matches neither.
- Create the six lists and three libraries. Turn on enforce unique values for Items.ThreadKey, Mail.MessageId and Acknowledgements.ThreadKey before any rows exist.
- Fill the Queues list by hand, one row per kind, with a real person in Owner and Manager and a real number in ClockHours. Nothing below works until this table is somebody's.
- Build Flow 1 with classification hard-coded to other. Confirm that a thread of three emails produces one item with three message ids, and that attachments reach the library. Everything after this is refinement.
- Add the spam and complaint branches, in that order, before the acknowledgement step. Verify by reading the flow top to bottom that no path from complaint reaches a send.
- Add the acknowledgement, then attach your classifier so Kind stops being other.
- Build Flow 2. The approval precedes the Releases row and the Releases row precedes the send, with nothing between them.
- Build Flows 3 and 4, then connect Power BI. Acknowledged Complaints and Unowned Items go on the front page.
7. Four traps specific to this build
The hash the standard asks for has nowhere to come from
Checks three and nine both want a content hash: on the filed attachment, and on the quarantined message. Microsoft 365 has no action in the box that returns one. You have two honest options. Add a step you own that computes the hash and store it, and the standard is met. Or store what you do have, the message id plus the library item, call the column Fingerprint rather than Hash, and tell the Invoice Check build that the artefact is identified by file and message rather than by content. What you must not do is write a file size or an ETag into a column called Hash, because everything downstream will treat it as one.
Junk gets there before the flow does
The mail trigger watches the folder you point it at. Exchange files a great deal of the mail this standard calls spam into Junk before any flow sees it, and Junk clears itself out. The build can then truthfully say it never deleted a message while the mailbox quietly did. Decide deliberately: either trigger on the folder that actually receives everything, or accept that quarantine covers only what reaches the inbox and say so on the page where people go looking for a lost remittance.
Allow fill-in choices left switched on
A Choice column with fill-in enabled is a text column that looks closed. The first unusual message grows a ninth kind, and that kind has no Queues row, so it has no owner, no clock and no line in any measure. It is invisible in exactly the way the standard exists to prevent. Switch fill-in off on Kind, and check it again after anyone edits the column, because re-adding choices is where it gets turned back on.
Replies that leave without passing through the flow
The release gate is only worth what the send-as permissions are worth. Anyone who can open the shared mailbox in Outlook can reply from it, and the Releases list will never know. That is a permissions decision, not a flow: decide who holds send-as, and accept that the people who keep it are the people whose replies the record cannot vouch for. Reconciling the sent folder against Releases once a week is the cheap version of finding out.
8. Where the automation stops
- Every substantive reply: anything with a price, a date or a decision in it. A person writes or approves the words, and the release carries their name.
- The complaint, whole. What is said to that customer and when is theirs, and the build's only job is to get it in front of them untouched.
- Deciding a supplier invoice is genuine. This build files the attachment as evidence; the invoice check standard matches it and a person approves the payment.
- Reading everything the machine called other. That queue is the classifier admitting it does not know, and Other Share is the measure that says how often.
- The Queues table itself: who owns each kind, who it escalates to, and how long it may wait. A flow reads that table; it never writes to it.
The pass test. Stage one conversation in the real shared mailbox. Send a message to it from an address you control with a PDF attached, then reply to your own message twice in the same thread, then run the flow across all three. You must end with exactly one Items row carrying three message ids, one EvidenceIndex row pointing at the PDF sitting in the Attachments library, and an Owner and a Due that came off the Queues row rather than from anywhere in the flow. Then set Kind to complaint on a staged item and run Flow 1 against it: there must be no Acknowledgements row for that thread at all, and an EscalatedTo with a name in it. Fifteen minutes. If a second Items row appeared, the conversation id is not the key or the unique value is off, and one customer will get two answers to one question. If the PDF is an attachment on the list item rather than a file in the library, nothing downstream can query for it and the invoice check build starts from nothing. If the complaint got an acknowledgement, the complaint branch is sitting beside the send instead of before it.