← All white papersBuild plan · September 2026

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.

10 min read·Free to implement·Published by AI Pathway

Used by

  • AI Officer
  • Business Systems Analyst
  • Office Administrator

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 ruleEnforced byHow
Every message gets exactly one kind from a closed listKind, a Choice columnEight 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 idMail.FiledToThe 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 recordAttachments + EvidenceIndexThe 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 dateQueues listOwner 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 customerFlow orderThe 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 releaseApproval + ReleasesThe 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 itemItems.ThreadKey, uniqueThe 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 deletedQuarantine libraryThe 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

  1. Settle the id space: which system gives you job ids, which gives you supplier ids, and what happens to a message that matches neither.
  2. Create the six lists and three libraries. Turn on enforce unique values for Items.ThreadKey, Mail.MessageId and Acknowledgements.ThreadKey before any rows exist.
  3. 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.
  4. 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.
  5. 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.
  6. Add the acknowledgement, then attach your classifier so Kind stops being other.
  7. Build Flow 2. The approval precedes the Releases row and the Releases row precedes the send, with nothing between them.
  8. 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.

Related reading