← All white papersBuild plan · September 2026

Database Reactivation on Microsoft 365

Four plays with triggers you can query, a rank nobody sorts by hand, and a gate that runs on every send rather than on the view.

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

Used by

  • AI Officer
  • Business Systems Analyst
  • Marketing Manager
  • Operations Manager

Reactivation fails in a predictable way. Somebody exports the database, sorts it by how long ago each person was last contacted, and works down the list until the awkwardness outweighs the optimism. The standard replaces that with four plays and a ranking. This is what the plays and the ranking look like when they live in a tenant.

In short

  • What this is

    A build plan for the database reactivation standard on Microsoft 365. Three lists, three flows, six measures.

  • The core rule

    If you cannot write a play as a filter over the lists, it is not a play. That constraint is what makes the whole thing measurable.

  • What it does not do

    Deciding somebody is finished with, or that a relationship is over, is a person's call. The system surfaces and records.

  • The hard part

    The cooling-off check across plays. Each play is sensible alone and together they are a nuisance.

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 database reactivation build standard first. It owns the four plays, the six ranking rules and the openers. Where it and this page disagree, it wins. The shared conventions for this stack are in building the standards on Microsoft 365.

Like the debtor standard, this one specifies behaviour rather than record shapes. The lists below are the minimum that satisfies its checklist, not objects it names. The plays are yours to define; what is not negotiable is that each one is expressible as a query.

2. The lists, with their columns

Three lists. Play and Rank are values on the row rather than views over it, which is the difference between a campaign you can measure and a spreadsheet somebody sorted.

For your developer. Show the fields
Site: OwnList

LIST  People
  ContactRef      Text            indexed. your source system's id
  Name            Text
  Channels        Text            what you may actually reach them on
  LastContactedAt DateTime        maintained by the contact flow
  LastOutcome     Choice          spoke | no_answer | replied | opted_out
                                  | not_now | converted
                                  the newest Contacts outcome, copied back
  Play            Choice          tenure | equity_check_in | investor
                                  | lapsed_appraisal
                                  the play that selected them, or blank
  PlaySetAt       DateTime
  Rank            Number          WRITTEN BY THE RANK FLOW ONLY
  RankReason      Text
  SourceAsOf      DateTime        when the source system last refreshed this

LIST  Contacts              (append only)
  ContactRef      Text            indexed
  Play            Choice          tenure | equity_check_in | investor
                                  | lapsed_appraisal
                                  which play produced this attempt
  Channel         Choice          call | sms | email
  AttemptedAt     DateTime
  Outcome         Choice          spoke | no_answer | replied | opted_out
                                  | not_now | converted
  Notes           Text
  By              Person

LIST  Suppressions
  ContactRef      Text            indexed
  Reason          Choice          opted_out | complaint | duplicate | deceased
                                  | do_not_contact | other
  AddedBy         Person
  AddedAt         DateTime
  ClearedAt       DateTime        blank means live
  Note            Text

Add your own trigger fields to People: whatever the plays actually filter on.

opted_out is permanent. It is the one reason with no ClearedAt path.

The trigger fields are deliberately left to you: they are whatever your plays filter on, and a standard that guessed them would be describing somebody else's business. What matters is that they are columns, indexed, and refreshed by something other than a person.

3. Every check, and what enforces it

The checkEnforced byHow
Every play has a trigger you can queryPlay flowEach play is a stored query writing Play onto the row. If a play cannot be expressed as a filter over the lists, it is a mood and it does not ship.
Rank is computed, not sortedRank flowRank and RankReason are written by a scheduled flow. A person can disagree with the order because they can read why.
Suppression is checked before every contactContact flowInside the send step, per send. Not a view filter, which is per person and gone the moment somebody opens the list another way.
One contact per person per window, across playsContact flowTwo plays can both select the same person. The cooling-off check is what stops them both firing, and it reads Contacts rather than the play that is running.
The opener names something specific and checkableTemplate bindingThe message is generated from the row's own fields. A template with no merged facts is the generic opener the standard says does not work.
Every attempt is recorded, answered or notContact flowAppend-only Contacts. No answer is an outcome, and it is the one people do not write down.

4. The three flows

For your developer. Show the fields
FLOW 1  Assign the play
  trigger  scheduled, daily
  logic    clear Play where the trigger no longer holds
           for each play, in the order you have ranked them:
             select rows matching that play's query and with
             no play already set today
             write Play and PlaySetAt
  note     one play per person per run. A person who matches two
           gets the higher-priority one, and the other does not
           silently also fire later in the day.

FLOW 2  Rank
  trigger  scheduled, after FLOW 1
  logic    score the selected rows on the standard's six rules,
           write Rank and RankReason

FLOW 3  Contact, with both gates inside it
  trigger  an operator action or an approved run
  guard 1  Suppressions: any live row stops the send
  guard 2  cooling off: no Contacts row for this ContactRef
           inside your window, whatever play it came from
  logic    render the opener from the row's own fields
           send, then append Contacts whatever happened
           update People.LastContactedAt and LastOutcome
  never    pre-filter the send list and skip the guards. Two plays
           selecting the same person is exactly the case a
           pre-filtered list cannot see.

Guard 2 is the one that is always left out of a first build and always added after somebody complains. Building it first costs an hour.

5. The Power BI model and its measures

For your developer. Show the fields
Selected = CALCULATE ( COUNTROWS ( People ), NOT ISBLANK ( People[Play] ) )

Worked =
CALCULATE (
    DISTINCTCOUNT ( Contacts[ContactRef] ),
    Contacts[AttemptedAt] >= TODAY () - 7
)

Actioned Rate = DIVIDE ( [Worked], [Selected] )

Reply Rate =
DIVIDE (
    CALCULATE (
        COUNTROWS ( Contacts ),
        Contacts[Outcome] IN { "spoke", "replied", "converted" }
    ),
    COUNTROWS ( Contacts )
)

Opt Outs = CALCULATE ( COUNTROWS ( Contacts ), Contacts[Outcome] = "opted_out" )

Double Contacts =                       -- should be zero
COUNTROWS (
    FILTER (
        SUMMARIZE ( Contacts, Contacts[ContactRef], "n", COUNTROWS ( Contacts ) ),
        [n] > 1
    )
)

Report Reply Rate and Opt Outs side by side, always. A play that lifts replies while lifting opt-outs faster is burning the list to make a chart look better, and seeing them apart is how that gets missed for a quarter.

6. The order to build it in

  1. Create the three lists. Index ContactRef everywhere.
  2. Load Suppressions before People. An empty suppression list on day one is how the first run reaches somebody who asked you not to.
  3. Build Flow 3 with both guards, and test it before any play exists.
  4. Run the pass test below. Two minutes, no real contact involved.
  5. Write one play as a query. One. Prove it selects who you expected before writing the other three.
  6. Build Flow 1 with that single play, then Flow 2.
  7. Connect Power BI. Put Double Contacts on the front page and expect it to be zero.

7. Four traps specific to this build

Plays as saved views instead of stored values

A view is a filter somebody applies. A play has to be a value on the row, written by a flow, or you cannot count how each play converted and you cannot stop two of them selecting the same person.

No cooling-off check across plays

The single most common way this build annoys people. Each play looks reasonable alone; together they ring the same person twice in a week. The check belongs in the contact step and it reads contact history, not the current play.

The source list is a spreadsheet somebody maintains

If the list of people is exported from a CRM once and then edited in the tenant, the two diverge and the edited copy is the one that gets used. Sync it, or make the tenant the record. Not both.

Sending from a personal mailbox connection

A flow built on somebody's own account stops the day they leave or change their password, and everything it sent is in their sent items rather than a shared record. Use a shared mailbox, and write the record to the list regardless.

8. Where the automation stops

  • Deciding a contact is finished with. The system surfaces; a person retires.
  • Clearing a suppression, and never for an opt-out, which has no clearing path at all.
  • Writing the offer. The opener is merged from real fields, but what you are offering is a commercial decision.
  • Deciding to widen a play because the list is running dry. That is the moment the plays stop having triggers.

The pass test. Two staged checks, neither needing a real contact. First, add a Suppressions row for a test person with reason opted_out, then try to contact them through the normal path: the send must refuse and the refusal must be recorded. Second, append a Contacts row for another test person dated today, then run a different play that also selects them: the cooling-off guard must stop the second contact even though the play is new. If the first sends, the gate is a filter. If the second sends, you have as many contact policies as you have plays, and the person on the other end experiences all of them.

Related reading