Invoicing Out on Microsoft 365
Completion is the trigger, the book is the only source of a rate, and nothing goes out without a release. SharePoint can refuse the second invoice outright; the write into the ledger is the one part it will not do for you.
Used by
- AI Officer
- Business Systems Analyst
- Bookkeeper
- Finance Manager
How to get this live in your business
The arithmetic on an invoice is the easy half and it is the half every tool sells. The hard half is that the invoice exists on the day the job finished, exists once, carries a rate somebody can point at, and carries the name of whoever said send. Three of those four are list design. The fourth is a gate, and Power Automate will happily let you build a version that has no gate at all.
In short
What this is
A build plan for the invoice out standard on Microsoft 365. Six lists, a library, four flows, seven measures.
The core rule
Every line traces to a job line and takes the book's rate at draft time. A code the book does not hold parks the invoice. No rate is ever entered on a form.
What it does not do
It never sends without a release. A named person releases, or a rule that person wrote down once with a threshold on it, and either way the release is on the row.
The hard part
The job marked complete twice, and the ledger write that returns nothing. A flow run that succeeded is not the same fact as an invoice that exists.
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 out 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: how this tenant learns that a job is complete. If the job system can send a notification to a mailbox you can monitor, that is your trigger. If it cannot, a scheduled flow reads completions on a cadence and the invoice is late by at most that cadence, which is a number worth choosing on purpose. A Tradify business has neither, and the standard already says so: the job lines are entered by a person and everything below still applies from the Jobs list onwards.
Then decide where the price book lives and what an edition of it means. The standard asks every rate to come from the book read at draft time, and a book with no edition on it cannot answer the only question that matters six months later, which is which rate was in force when this invoice was drafted.
2. The lists, with their columns
For your developer. Show the fields
Site: InvoiceOut
LIST Jobs
JobRef Text indexed
CustomerRef Text indexed
Status Choice open | complete
CompletedAt DateTime
UnderContract Boolean a building contract applies
ContractStage Text named stage. Blank here plus UnderContract = yes is a park
JobOwner Person
LIST JobLines
JobRef Text indexed
Code Text indexed. The catalogue code
Qty Number
Kind Choice callout | labour | material | variation
AcceptanceRef Hyperlink into SignedAcceptances. Required on a variation, blank on everything else
LIST PriceBook
Code Text indexed
Description Text
Rate Currency
Edition Text indexed. The edition, not a date stamp
EffectiveFrom DateTime
EffectiveTo DateTime blank means in force
LIST Invoices (one row per job, and the list enforces it)
JobRef Text indexed, UNIQUE. The second write is refused, not checked for.
ExternalId Text the ledger's own id. Blank is a failure
BookEdition Text which edition the rates came from
Stage Text
Total Currency
Status Choice draft | parked | released | sent
ParkReason Choice not_in_pricebook | unsigned_variation
| write_failed | no_stage
ParkOwner Person
DraftedAt DateTime
ReleasedBy Person blank until a person releases
ReleasedByRule Text or the rule's name, never both
ReleasedAt DateTime
SentAt DateTime
LIST InvoiceLines
JobRef Text indexed
JobLineId Number the JobLines item this came from
Code Text
Qty Number
Rate Currency written by the flow. Off every form.
Kind Choice callout | labour | material | variation
LIST ReleaseRules (a person writes this down once)
Name Text indexed. what ReleasedByRule records
Threshold Currency
SetBy Person
SetAt DateTime
LIBRARY SignedAcceptances
AcceptanceRef Text indexed
JobRef Text
SignedAt DateTime
Invoices is writable by the flow's account only. Everyone else reads.Unique values on the indexed JobRef is the single most useful line here. The standard says one invoice per job; a flow that checks for an existing row first is a flow with a race in it, and SharePoint refusing the second write is a guarantee rather than a good intention. Rate lives on InvoiceLines and is removed from every form, because a Currency column a person can see is a Currency column a person will eventually correct.
3. Every rule, and what enforces it
| The rule | Enforced by | How |
|---|---|---|
| The job's completion is the only trigger | Jobs list, Flow 1 | Nothing creates an Invoices row except a job moving to complete. Invoices is not writable by hand, so there is no second way in and no Sunday night version of this workflow. |
| Every invoice line traces to a line on the job | InvoiceLines.JobLineId | Every line carries the JobLines item id it came from. A line with a blank id is not a traced line, and the measure that counts them is how you find out the flow drifted. |
| Every line is priced from the price book, read at draft time | PriceBook lookup, BookEdition | The flow reads the book once per draft and stamps the edition on the invoice. The rate is never on a form, so there is nothing to type past. |
| A code the book does not hold parks the invoice | ParkReason choice | Four choices, fill-in off. A park always names one of the standard's four reasons, so the parked queue can be grouped and the book's gaps become a list rather than a feeling. |
| A variation is billed only with a signed acceptance attached | JobLines.AcceptanceRef, Flow 1 | Before any pricing, the flow checks that every variation line points at a file in SignedAcceptances. A blank reference parks the whole invoice, not just the line. |
| One invoice per job, written once | Unique values on JobRef | The list refuses the second row. The second completion event updates the first one or does nothing, which is what the standard asks for. |
| Nothing is sent without a release | Approval, ReleaseRules, Flow 4 | Flow 4 will not write SentAt unless ReleasedBy or ReleasedByRule is filled. The rule may release only under its own threshold; everything over it goes to the person who set it. |
| The record is retrievable by job | JobRef indexed everywhere | One key across all six lists. A disputed bill is a single filter that returns the lines, the rates, the edition, the release and the send time. |
4. The four flows
For your developer. Show the fields
FLOW 1 Draft on completion
trigger a Jobs item is modified and Status = complete
logic stop if an Invoices row already exists for this JobRef
read the JobLines rows for the job
park checks, before any pricing:
a variation line with no AcceptanceRef -> unsigned_variation
UnderContract = yes and ContractStage blank -> no_stage
a code the PriceBook does not hold -> not_in_pricebook
otherwise write InvoiceLines from JobLines at the book's
rates, stamp BookEdition, create Invoices as draft
never write a Rate that did not come from a PriceBook row, and
never price a line before the park checks have run.
FLOW 2 Get it into the ledger
trigger an Invoices item reaches Status = draft
logic hand the row to whatever holds the ledger credentials
store what comes back in ExternalId
nothing back -> Status = parked, ParkReason = write_failed
never treat a successful flow run as a successful write. The run
finishing and the invoice existing in Xero or MYOB are two
different facts and only one of them is in the ledger.
FLOW 3 Release
trigger ExternalId is written
logic if a ReleaseRules row covers Total, write ReleasedByRule
and ReleasedAt, and stop
otherwise start an approval to the person who set the rule
write ReleasedBy and ReleasedAt from the outcome
never leave the release in the approval history. The history is
not the record a disputed bill gets read from.
FLOW 4 Send, and close the record
trigger ReleasedBy or ReleasedByRule is written
logic send from the ledger, then write SentAt
never touch a parked invoice. Parked never reaches this flow,
and that is the whole point of parked being a status.Flow 1 does the park checks before the pricing on purpose. A build that prices first and checks afterwards has already produced a plausible total for an unsigned variation, and a plausible total is the thing somebody sends by accident.
5. The Power BI model and its measures
For your developer. Show the fields
Sent Without Release = -- must be zero. Front page.
CALCULATE (
COUNTROWS ( Invoices ),
NOT ISBLANK ( Invoices[SentAt] ),
ISBLANK ( Invoices[ReleasedBy] ),
ISBLANK ( Invoices[ReleasedByRule] )
)
Write Not Landed = -- drafted, no id back
CALCULATE ( COUNTROWS ( Invoices ), ISBLANK ( Invoices[ExternalId] ) )
Drafted Same Day =
VAR OnTheDay =
FILTER (
Invoices,
DATEDIFF ( RELATED ( Jobs[CompletedAt] ), Invoices[DraftedAt], DAY ) = 0
)
RETURN
DIVIDE ( COUNTROWS ( OnTheDay ), COUNTROWS ( Invoices ) )
Parked Now =
CALCULATE ( COUNTROWS ( Invoices ), Invoices[Status] = "parked" )
Parked Ageing Days =
AVERAGEX (
FILTER ( Invoices, Invoices[Status] = "parked" ),
DATEDIFF ( Invoices[DraftedAt], TODAY (), DAY )
)
Book Gaps = -- group this by code
CALCULATE ( COUNTROWS ( Invoices ), Invoices[ParkReason] = "not_in_pricebook" )
Released By Rule Share =
DIVIDE (
CALCULATE ( COUNTROWS ( Invoices ), NOT ISBLANK ( Invoices[ReleasedByRule] ) ),
CALCULATE ( COUNTROWS ( Invoices ), NOT ISBLANK ( Invoices[SentAt] ) )
)Book Gaps grouped by code is the measure that pays for the build. Every park with reason not_in_pricebook is a line somebody used to price from memory, and the same three codes will be at the top of that chart every month until someone puts them in the book.
6. The order to build it in
- Stand up PriceBook as a list with an Edition column, even if the first edition is one export out of the job system. A rate with no edition cannot be defended in the conversation this build exists for.
- Create Jobs and JobLines and get completion landing in them, by monitored mailbox or by a scheduled read. Until a job arrives with Status complete there is nothing downstream to test.
- Create Invoices with JobRef indexed and unique values switched on, before any flow writes to it. Turning it on later means first deciding which of the duplicates was real.
- Build Flow 1 with the park checks only, and no pricing at all. Prove an unsigned variation parks before you prove a clean job prices.
- Add the pricing to Flow 1 and take Rate off every form on InvoiceLines.
- Wire Flow 2, and prove the blank ExternalId path parks. This is the step most builds skip, and the one that leaves invoices that exist only in SharePoint.
- Build Flow 3 with the person's approval only. Add the threshold rule once the person's path works, not before.
- Build Flow 4, confirm a parked invoice cannot reach it, then connect Power BI with Sent Without Release on the front page.
7. Four traps specific to this build
Leaving Rate on a form
A Currency column that renders in the list view is a Currency column somebody will correct when a customer queries a price, and the correction will be right that once and invisible forever after. Write it from the flow, remove it from the new and edit forms, and let the only path to a rate be the book.
Reading the modified trigger as the completion event
A when-an-item-is-modified trigger fires on every column change, including the ones Flow 2 and Flow 3 make on the way back. Condition the trigger on Status being complete, guard Flow 1 with a read for an existing invoice, and let the unique index be the thing that actually stops the duplicate.
Counting a successful run as a successful ledger write
The standard is explicit that a null external id is a failed write, not a success. In Power Automate the natural shape is a run that ends green with an empty variable. Make the blank id set Status to parked with reason write_failed, or you will have a queue of invoices that exist only in your own tenant and nowhere the bookkeeper looks.
Using the approval as the release record
Approvals keep their own history, and it is a fine audit trail and a terrible record. The outcome must be written onto the Invoices row as ReleasedBy and ReleasedAt in the same flow. The day a customer disputes the bill, the answer has to be readable from the invoice, not from somebody's Teams activity.
8. Where the automation stops
- The release, or the threshold rule that stands in for it. A person writes that number down once, under their own name, and everything over it comes back to them.
- Pricing a line the book does not hold, and the separate decision about whether it belongs in the book. The park is the ask; nobody automates the answer.
- The variation, and every argument about what was actually agreed. Agreed on the phone is not signed, and the build cannot tell the difference between a missing file and a missing conversation.
- Anything that assesses, serves or argues a claim under security of payment law. This build raises a stage invoice with the stage named on it; the claim stays with the builder and their advisor.
- Credit notes, write-offs and discounts. This raises invoices and never reduces one, which is also why nothing here has a negative Currency column.
The pass test. Stage one job with four lines: a callout, two hours of labour, a material the book holds, and a variation with no acceptance file against it. Mark it complete, then mark it complete again an hour later. You must get exactly one Invoices row for that JobRef, parked with ParkReason unsigned_variation, with no rate written on anything and no SentAt. Then put the signed acceptance in the library, point the job line at it, and let the flow run again: the invoice should draft at the book’s rates with BookEdition stamped on it. Ten minutes on test data. If a second Invoices row appeared, unique values is not switched on and no amount of flow logic will hold that line. If the invoice drafted with the variation priced, the flow is reading the job lines and not the acceptance, and the first customer to ask what they signed will find that out before you do.