← All white papersBuild plan · September 2026

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.

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

Used by

  • Business Systems Analyst
  • Finance Manager
  • Operations Manager

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 ruleEnforced byHow
Every figure on the report names a definition in the registerFigures.DefinitionIdRequired 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 reusedRun flowThe 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 readFigures.AsOfCaptured 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 changedRecurrence triggerThe 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 reasonRun after failureThe 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 carryNarrative stepThe 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 elseAudienceMeasuresEach 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 versionReports.Version + permissionsCorrecting 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. Add the remaining measures, the threshold comparison against the prior published figure, and the narrative assembled from the figure rows.
  8. 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.

Related reading