Checking Subcontractor Invoices on Microsoft 365
Four checks in a fixed order, stopping at the first failure. The order is the claim, and a parallel implementation quietly discards it.
Used by
- Business Systems Analyst
- Contract Administrator
- Commercial Manager
How to get this live in your business
Free. The Invoice Check Build Standard: the objects, the checks in order, and the pass test.
Ready for
The thing that makes this workflow work is not the extraction. It is that the checks run in an order, stop when one fails, and leave a record that outlives the decision somebody made about it. Power Automate makes the first of those slightly awkward and the second entirely up to you, which is why both are called out here.
In short
What this is
A build plan for the invoice check standard on Microsoft 365. Three lists, a library, three flows, six measures.
The core rule
The checks are ordered because each one assumes the previous passed. Running them in parallel produces differences that are not real.
What it does not do
It never approves or rejects a payment. It produces a difference in dollars and the evidence behind it, for a person who is accountable for the decision.
The hard part
The cumulative position. Every individual variance is defensible; the run of them is the thing nobody is looking at.
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 invoice check 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 the invoice is being checked against. A purchase order, a subcontract, a schedule of rates. If that reference does not exist in a queryable form, no amount of extraction helps, and standing it up is the real first task.
2. The lists, with their columns
For your developer. Show the fields
Site: InvoiceChecks
LIBRARY Originals
InvoiceRef Text indexed. The file, kept as received.
ReceivedAt DateTime
LIST Invoices
InvoiceRef Text indexed
SupplierRef Text indexed
Amount Currency
OrderRef Text PO, subcontract or rates schedule
OrderAmount Currency
SourceDocument Hyperlink into Originals
ReceivedAt DateTime
Outcome Choice pass | exception | not_checked
LIST CheckResults (one row per check, per invoice)
InvoiceRef Text indexed
Check Choice rate | quantity | variation | contract_sum
Sequence Number 1 to 4. The order is data, not code.
Status Choice pass | fail | not_run
Expected Text
Found Text
Difference Currency dollars. Not a percentage, not a score.
Confidence Number
ConfidenceBasis Text so a person can discount it
LIST Exceptions (a record, not a notification)
InvoiceRef Text indexed
FailedCheck Choice rate | quantity | variation | contract_sum
NeverRan Text the checks after the failure
Difference Currency
Queue Choice needs_decision | waiting | done_reversible
| blocked
OpenedAt DateTime
DecidedBy Person blank until decided
DecidedAt DateTime
Evidence Hyperlink what informed the decision
Nothing is deleted on resolution. Deciding writes two fields.Sequence is stored rather than implied by the flow's shape, so the order is inspectable and a Power BI page can show which check stops the most invoices. A reader who wants to know where the money leaks asks that question first.
3. Every check, and what enforces it
| The check | Enforced by | How |
|---|---|---|
| Four checks in a fixed order, stopping at the first failure | Check flow | A terminating loop, not four parallel branches. The checks after a failure are recorded as not_run rather than silently skipped, because which ones never ran is information. |
| Differences are dollars, not scores | List design | Difference is a Currency column. No percentage, no confidence-weighted total. A person approving a payment needs an amount. |
| An exception is a record, not a notification | Exceptions list | Raised as an item with a queue, an owner and an opened-at. A Teams card may announce it; the card is not the exception. |
| The record survives the decision | List design | Deciding sets DecidedBy and DecidedAt. It does not delete the row or clear the difference, so the position is still readable next quarter. |
| The cumulative position, not just this claim | Measure | Cumulative is computed in the model across the order, because a run of small accepted differences is the failure mode a per-invoice view cannot see. |
| Confidence carries its basis | List design | ConfidenceBasis is stored next to the number so a person can discount it. A bare confidence score is a number nobody can argue with, which is worse than none. |
4. The three flows
For your developer. Show the fields
FLOW 1 Intake
trigger a file arrives in Originals, or an email with an attachment
logic store the original unchanged
extract InvoiceRef, SupplierRef, Amount, OrderRef
create the Invoices row with Outcome = not_checked
never modify the original. Extraction writes a row beside it.
FLOW 2 The four checks, in order
trigger an Invoices item is created
logic for sequence 1 to 4, IN A SEQUENTIAL SCOPE:
run the check, write a CheckResults row
if status = fail:
write the remaining checks as not_run
raise an Exception with NeverRan listing them
set Invoices.Outcome = exception
TERMINATE the scope
all four pass -> Outcome = pass
never parallel branches. The order carries the meaning, and
a quantity difference computed on a wrong rate is noise
presented as a finding.
FLOW 3 Decide
trigger an operator action on an Exception
logic write DecidedBy, DecidedAt, Evidence and the new Queue
never delete the row, clear Difference, or reopen it as new.
The cumulative measure reads the decided ones.Power Automate makes parallel the easy shape and sequential the deliberate one. That is the whole reason this flow is written out here rather than left as an exercise.
5. The Power BI model and its measures
For your developer. Show the fields
Exceptions Open =
CALCULATE ( COUNTROWS ( Exceptions ), ISBLANK ( Exceptions[DecidedAt] ) )
Difference Open =
CALCULATE ( SUM ( Exceptions[Difference] ), ISBLANK ( Exceptions[DecidedAt] ) )
Cumulative By Order =
CALCULATE (
SUM ( Exceptions[Difference] ),
ALLEXCEPT ( Invoices, Invoices[OrderRef] )
)
Stopped At = -- which check stops the most invoices
CALCULATE ( COUNTROWS ( CheckResults ), CheckResults[Status] = "fail" )
Never Ran =
CALCULATE ( COUNTROWS ( CheckResults ), CheckResults[Status] = "not_run" )
Decision Latency Days =
AVERAGEX (
FILTER ( Exceptions, NOT ISBLANK ( Exceptions[DecidedAt] ) ),
DATEDIFF ( Exceptions[OpenedAt], Exceptions[DecidedAt], DAY )
)
Unchecked = -- should be near zero
CALCULATE ( COUNTROWS ( Invoices ), Invoices[Outcome] = "not_checked" )Cumulative By Order is the measure the standard is really arguing for. Put it on the front page against each order, because the pattern it shows is invisible in any view built one invoice at a time.
6. The order to build it in
- Stand up the reference the invoice is checked against. Without it nothing downstream means anything.
- Create the library and three lists. Index InvoiceRef everywhere.
- Build Flow 1 and confirm the original is stored unchanged with a row beside it.
- Build Flow 2 with check 1 only, in a sequential scope with the termination already in place. Add checks 2 to 4 afterwards.
- Verify a failure at check 2 writes checks 3 and 4 as not_run. This is the pass test.
- Build Flow 3, and confirm deciding leaves the row readable.
- Connect Power BI. Cumulative By Order goes on the front page.
7. Four traps specific to this build
Running the four checks in parallel
Parallel branches are the obvious Power Automate shape and they are wrong here. The order is the claim the standard makes: a rate failure makes the quantity comparison meaningless, and running it anyway produces a second difference that is not real. Use a sequential scope with a termination condition.
Storing the source document as an attachment only
Attachments cannot be queried and do not survive a list migration cleanly. Put the original in a library with a link on the row, so an exception can always be opened next to what it came from.
Deleting exceptions when they are resolved
The tidy instinct destroys the only evidence of the pattern. Resolution is two fields on the row. The cumulative measure depends on the closed ones still being there.
A confidence number with nothing behind it
If the extraction produces 0.82 and nothing says why, nobody can decide whether to trust it, so they either trust all of them or none. Store the basis in words, in the same row.
8. Where the automation stops
- Approving or rejecting a payment. The workflow produces a difference and evidence; the decision is a person's and it is recorded as theirs.
- Accepting a variation. That is a commercial decision with contractual consequences.
- Anything that assesses, chases or advises on a statutory payment claim. Security of Payment is a statutory process and it stays with the client and their advisor.
- Changing the reference. If the order is wrong, that is a conversation, not a correction a flow makes on the way past.
The pass test. Stage an invoice that fails the second check and passes the first. Run it. You must get exactly two CheckResults rows with a status of pass or fail, two more recorded as not_run, and one Exception whose NeverRan names those two. Then decide that exception and confirm the row is still there with its Difference intact and DecidedBy set. A minute on test data. If all four checks produced a difference, they ran in parallel and three of those numbers are meaningless. If deciding emptied or removed the row, the cumulative position across the order is already gone.