The Recurring Report on Microsoft 365
A figure is a stored row with its source and the time that source was read, not a tile that refreshed. The difference only shows up on the morning the feed is down, which is the morning it matters.
Used by
- Business Systems Analyst
- Finance Manager
- Operations Manager
How to get this live in your business
Most of this already exists in your tenant, and that is the problem. Power BI will refresh every morning and put a number on the wall, and on the morning the source cannot be read it will show yesterday’s number with exactly the same confidence and no mark on the tile. The hardest requirement in this standard is the one the stack does not do by default: publish a figure that says it could not be computed, on time, and leave last period’s figure exactly where it was.
In short
What this is
A build plan for the recurring report standard on Microsoft 365. Six lists, four flows, and one design decision that the rest of the page follows from.
The core rule
Every figure is stored with its definition, its source and the time that source was read. Power BI reads those rows. It does not produce them, because a failed refresh renders the last good data and says nothing.
What it does not do
It does not show last period's value when a source cannot be read, and it does not change a published report. A correction is a new version with a name and a reason on it.
The hard part
Not the query. Making could not be read into something the report is able to say, on a platform whose default behaviour is to keep showing the last number that worked.
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 recurring report 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.
Write the definitions register before you create a single list. One sentence per measure: which source, which query, which period, which filters, and who owns it. Revenue is invoiced revenue from the ledger for the calendar month, excluding credit notes, owned by the bookkeeper. The standard puts it plainly: the register is the product and the report is what it produces. Building the flows first gets you a working schedule that publishes numbers two people define differently, which is the thing you already have.
Then make the decision this whole page turns on. A figure is a row, not a tile. The obvious build here is a Power BI report over the lists on a scheduled refresh, and it fails two of the standard’s checks in ways nobody notices. When a refresh fails, the model keeps the last successful data and every visual renders it, which is last period’s value shown as current. And a visual over live lists recomputes history, so the report the lender read in August quietly becomes a different report once August’s data is tidied up. Compute each figure in a flow, store it with its source and its as-of time, and point Power BI at the stored rows.
Last, list the audiences. Not the recipients: the audiences, each with the measures it carries. A lender, a franchisor and the owner are three lists of measures, and the difference between them is a declaration somebody made rather than a filter somebody applied.
2. The lists, with their columns
For your developer. Show the fields
Site: Reporting
LIST Definitions (the register. A person writes these.)
DefinitionId Text indexed
Measure Text indexed. One live definition, ever.
Source Text the system, named the same way everywhere
QueryRef Text the stored query or flow step that computes it
Period Choice week | month
Filters Note in words, so the owner can check it
Owner Person REQUIRED. Told when the source cannot be read.
EffectiveFrom DateTime a date, formatted without a time
Status Choice live | superseded
LIST Audiences
AudienceId Text indexed
Name Text the owner, the lender, the franchisor
Recipients Person multi-value, or an address where they are outside
Channel Choice teams | email
LIST AudienceMeasures (the declaration. A join, not a string.)
AudienceId Text indexed
Measure Text indexed
LIST Reports (one row per audience, per period, per version)
ReportId Text indexed
Period Text indexed. 2026-W39 or 2026-09
AudienceId Text indexed
Version Number 1, then 2. Never edited.
ScheduledAt DateTime written when the period opens, before the run
PublishedAt DateTime when it went. Blank means it did not.
PublishedBy Person blank when the schedule published it
Reason Text blank on version 1. Required on 2 and after.
Status Choice published | superseded
Supersedes Text the ReportId this replaces
Narrative Note may describe. May not carry a number the figures do not.
LIST Figures (one row per measure, per report)
ReportId Text indexed
Measure Text indexed
Value Number blank when unavailable. Blank is a state.
Unit Choice dollars | count | percent | days
Unavailable Boolean REQUIRED. Yes means Value is blank on purpose.
UnavailReason Text the failure, or no_definition
DefinitionId Text required wherever Unavailable is No
Source Text written by the flow from the read
AsOf DateTime the time THAT source was read. Never typed.
LIST Thresholds
Measure Text indexed
Direction Choice up | down
Amount Number the move that matters
Notify Person REQUIRED. A threshold with nobody is a column.
Permissions: everybody reads Reports and Figures. Only the account
the flows run as contributes. An in-place edit is then not
available to the person who would make one.The standard's Version object is a row in Reports rather than a list of its own, because a version is a whole published report and everything the object carries has a column here: the report, the number, the reason, who published it and when. SharePoint's own versioning is not a substitute for that. It records edits to a row, and the thing being tracked is the succession of published reports.
3. Every rule, and what enforces it
| The rule | Enforced by | How |
|---|---|---|
| Every figure on the report names a definition in the register | Figures.DefinitionId | Required on any row where Unavailable is No. A declared measure the register does not define is still published, as a row with no value and the reason no_definition, rather than quietly left off the page. |
| One definition per measure, computed once per period and reused | Run flow | The flow reads the distinct measures any audience carries, computes each once, then writes the same value into every audience's figure row. Two views carrying the same measure cannot produce two numbers that happen to agree today. |
| Every figure carries the source it was read from and the time it was read | Figures.AsOf | Captured inside the action that reads each source, not at the end of the run. A report assembled at six from a ledger read at midnight then says midnight, which is the only thing an as-of time is for. |
| The report publishes at the scheduled time whether or not anything changed | Recurrence trigger | The publish step sits outside every condition, and the period's Reports rows are created before the run with ScheduledAt on them. A period that never published is a row with a blank PublishedAt, which can be queried. An absence cannot. |
| A figure whose source could not be read publishes as unavailable, with the reason | Run after failure | The read action's failure path writes the row with Unavailable yes, the reason, and a message to that definition's owner. There is no branch anywhere in the flow that copies a value from a previous period. |
| Prose describes figures that exist and never states a number they do not carry | Narrative step | The sentence is assembled by the flow from that report's figure rows, so a number that is not a figure has no way in. Where a model writes it instead, it is given the figure list and the flow refuses to publish a narrative carrying a numeral that is not on it. |
| Each audience sees the figures declared for it and nothing else | AudienceMeasures | Each audience gets its own report and its own figure rows, written from the declared list. Row level security is the wrong tool here, because the lender has no account in your tenant to be filtered by one. |
| A published figure changes only by a named person publishing a new version | Reports.Version + permissions | Correcting writes a fresh report row with the version raised, the person and the reason, and marks the old one superseded. Contribute on both lists belongs to the flow's account, so editing in place is not on the menu. |
4. The four flows
For your developer. Show the fields
FLOW 1 Open the period
trigger scheduled, at the start of each period
logic one Reports row per audience, with Period and ScheduledAt
set and PublishedAt left blank
why the period has to exist before the run, or a period the
schedule silently missed is an absence, and you cannot
query an absence. With this, it is a row.
never set ScheduledAt from the run. It is the promise, not
the record of what happened.
FLOW 2 The run
trigger scheduled, at the period's publish time
logic read Definitions where Status = live, for the measures
any audience carries
FOR EACH DISTINCT MEASURE, ONCE:
read the source
capture AsOf in the same action as the read
on failure (configure run after):
Value blank, Unavailable yes, UnavailReason = the
failure, and tell that definition's Owner
for each audience report:
one Figures row per declared measure, reusing the one
computation above
a declared measure with no live Definition ->
Unavailable yes, UnavailReason = no_definition
thresholds:
compare against the prior period's PUBLISHED figure
moved past Amount in Direction -> notify Notify, this run
prior period unavailable -> escalate to Notify.
A missing prior is never treated as no change.
build the Narrative from this report's figure rows
set PublishedAt, post to each audience's Channel
never skip the publish because nothing changed, publish before
ScheduledAt, or carry a value forward from last period.
FLOW 3 Correct
trigger a person asks for a correction, and gives a reason
logic new Reports row: Version + 1, Reason, PublishedBy = them,
Supersedes = the old ReportId
a fresh set of Figures rows, the corrected one and the
unchanged ones, each with its definition, source and as-of
old report Status = superseded
post to the same audience, saying what it supersedes
never update a Figures row, rewrite a Narrative, or move
PublishedAt on a published report. Each of those is the
in-place edit the standard is about.
FLOW 4 The record by period
trigger a person gives a period
logic return every version of every audience's report for it,
each figure with its definition, source and as-of, and
who published each version and why
never recompute. The answer is what was published, not what
the sources would say if you asked them now.Flow 1 is the one every build leaves out, and it is the only thing that makes a missed period visible. Without it, a week the schedule silently failed and a week that has not come round yet look identical in the data, and the first thing anybody checks is the data.
5. The Power BI model and its measures
For your developer. Show the fields
Figures Unavailable =
CALCULATE ( COUNTROWS ( Figures ), Figures[Unavailable] = TRUE () )
Unavailable Rate =
DIVIDE ( [Figures Unavailable], COUNTROWS ( Figures ) )
Undefined Figures = -- a declared measure nobody defined
CALCULATE ( COUNTROWS ( Figures ), Figures[UnavailReason] = "no_definition" )
Figures Without A Definition = -- must be zero
COUNTROWS (
FILTER (
Figures,
Figures[Unavailable] = FALSE () && ISBLANK ( Figures[DefinitionId] )
)
)
Periods Missed = -- must be zero
COUNTROWS (
FILTER (
Reports,
ISBLANK ( Reports[PublishedAt] ) && Reports[ScheduledAt] < NOW ()
)
)
Published Late Minutes =
AVERAGEX (
FILTER ( Reports, NOT ISBLANK ( Reports[PublishedAt] ) ),
DATEDIFF ( Reports[ScheduledAt], Reports[PublishedAt], MINUTE )
)
Staleness Hours = -- how old the numbers were when it went
AVERAGEX (
FILTER ( Figures, NOT ISBLANK ( Figures[AsOf] ) ),
DATEDIFF ( Figures[AsOf], RELATED ( Reports[PublishedAt] ), HOUR )
)
Corrected Reports =
CALCULATE ( DISTINCTCOUNT ( Reports[Period] ), Reports[Version] > 1 )
-- Relationship: Figures to Reports, many to one on ReportId.
-- Filter Reports[Status] = "published" on any page a person reads,
-- or superseded versions double every count on it.This model reports on the reporting rather than on the business. The figures the lender reads are the rows in the Figures list, published at their time with their as-of on them. A Power BI page over the same list is a fine way for the owner to look at them and it is not the report, because it recomputes when the lists change and a published report does not.
6. The order to build it in
- Write the register with a named owner against every measure. One sentence each: source, query, period, filters. Nothing below is worth building until two people agree on those sentences, which is the part that takes a fortnight.
- Create the six lists and set the permissions while the lists are empty: everybody reads Reports and Figures, and only the account the flows run as contributes. Retrofitting this after people have been editing rows is a different and much worse conversation.
- Declare the audiences and their measures, starting with the internal one. The lender's view is the same machinery with a shorter list, so there is nothing to learn from building it first.
- Build Flow 1 and let one period open with nothing behind it. A Reports row with a blank PublishedAt is the thing you will use to prove everything after this works.
- Build Flow 2 with one measure. Check the as-of on the figure row is the time the source was read and not the time the run finished, because those are the same number on the day you build it and never again.
- Break that source on purpose and run again. The figure must publish as unavailable with the reason, the owner must be told, and the report must still go out. This is the check most builds never test and the first one to fail in production.
- Add the remaining measures, the threshold comparison against the prior published figure, and the narrative assembled from the figure rows.
- Build Flow 3, correct a figure, and confirm version one is still readable with its original value. Then connect Power BI over Reports and Figures, with Periods Missed on the front page.
7. Four traps specific to this build
A refreshing dashboard instead of a published report
When a scheduled refresh fails, the model keeps the last data that loaded and every visual renders it, confidently, with nothing on the tile to say so. That is precisely the figure this standard forbids: last period's value shown as current. The same shape breaks the correction rule too, because a visual over live lists recomputes August whenever August's data is touched, so the report somebody relied on is not the report that is there now. Power BI is where the numbers get looked at. The published figure is a row with an as-of time on it.
One timestamp at the end of the run
Assembling every figure and stamping them all with the time the report was built makes the as-of decorative, and it is the most natural way to write the flow. A ledger read at midnight and a job system read at six must each say so, because the whole use of an as-of time is telling a reader which numbers are from when. Capture it inside the action that reads each source.
The audience as a filter
Filtering one report per recipient is a view, and a view is per person and per session. It also does not work at all for the audience that matters most: a lender does not have an account in your tenant to be filtered by. Write each audience its own figure rows from its declared measures. Row level security is the right tool inside the business and the wrong tool for anything that leaves it.
A model writing the commentary from the sources
If the sentence comes from the same prompt that summarised the data, it will state a percentage nobody computed, printed directly under numbers that were, and the reader has no way to tell them apart. Build the sentence from the figure rows. If a model writes it, give it the figure list and nothing else, and have the flow refuse to publish a narrative carrying a numeral that is not on that list. Checking numerals in a flow expression is fiddly, which is the honest argument for the template.
8. Where the automation stops
- Every correction, published as a new version with their name and the reason. The flow builds the version. It never decides that a figure was wrong.
- Setting a threshold, and who is told when it is crossed. A threshold with nobody named on it is a column, and it will be read as a control for about a year.
- What a figure means to a lender, a franchisor or a board. The sentence in the register is a person's, and it is the part no flow can check.
- Whether a report goes outside the business at all. Adding an audience is a decision about disclosure, not a row somebody adds because the data was already there.
- Reading a source your tenant has no supported way to read. The standard requires the as-of time to come from the read itself, so a number a person keys in cannot honestly carry one. Either the connection gets built or that measure stays off the report as unavailable. A typed timestamp dressed as a read is the exact failure this check exists to catch.
The pass test. Two consecutive periods, on the real sources, for one audience. Before the second period’s publish time, break one source on purpose: revoke the connection, or point its query at something that is not there. Then do nothing and let the schedule run.
The second period must publish at its scheduled time with every other figure intact. The broken one must be a row with a blank value, Unavailable set, a reason naming the failure, and that definition’s owner told. Open the first period’s report afterwards: its figures must be exactly what they were, with the same as-of times. Set a threshold on one measure you know moved, and confirm the named person was told in that run rather than the next one. Then correct one published figure as yourself and check version one is still readable with its original value in it.
If the second period showed the first period’s number, you built a dashboard. If the report did not go out at all, the schedule is conditional on the data, and the week it matters is the week it is silent. If opening the first report showed different numbers than it showed on the day, the figures are being recomputed and nothing was ever published in the sense this standard means.