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.
Used by
- AI Officer
- Business Systems Analyst
- Marketing Manager
- Operations Manager
How to get this live in your business
Free. The Database Reactivation Build Standard: the objects, the checks in order, and the pass test.
Ready for
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 check | Enforced by | How |
|---|---|---|
| Every play has a trigger you can query | Play flow | Each 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 sorted | Rank flow | Rank 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 contact | Contact flow | Inside 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 plays | Contact flow | Two 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 checkable | Template binding | The 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 not | Contact flow | Append-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
- Create the three lists. Index ContactRef everywhere.
- Load Suppressions before People. An empty suppression list on day one is how the first run reaches somebody who asked you not to.
- Build Flow 3 with both guards, and test it before any play exists.
- Run the pass test below. Two minutes, no real contact involved.
- Write one play as a query. One. Prove it selects who you expected before writing the other three.
- Build Flow 1 with that single play, then Flow 2.
- 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.