Independent demo by The AI Pipe. Fictional staff. No health data. Not connected to Mentella’s systems.

A welcome letter for every new therapist, sent when your team says yes.

An automation goes through the hiring sheet every 15 minutes and sends each approved letter once. A separate safety check watches it. When something needs a person, one short note says what happened, since when, who acts, and what to do.

Proposed workflow using Google Sheets and Apps Script, for the welcome email only. Your team would choose the sending rule.

See the stopped-automation alert

The hiring sheet this morning

Scenario clock: Monday, September 21, 9:00 AM. Choose a name to see what that person receives.

From
Mentella Health, from a dedicated sending account
Reply-to
a team inbox someone reads (your choice)
To
maya.okafor@example.org
Subject
Welcome to Mentella Health, Maya

Dear Maya,

Welcome to Mentella Health. We are very glad you are joining us, and we are looking forward to your start on Monday, October 5.

[Next step, written by your team: who will be in touch, and about what.]

If you have a question before then, simply reply to this email. [Who reads the replies, written by your team.]

Warmly, [Name], for the Mentella Health team

An example for your team to rewrite and approve. Dotted words come from the hiring sheet; bracketed lines are yours to write. “Sent” means the mail service accepted the letter, not that it was read.

Four ordinary situations, replayed

Each one starts from its own empty sheet, on its own day, with its own fictional people, and runs the same decision code as the tests, here in your browser, on a scenario clock. The automation goes through the hiring sheet every 15 minutes; a separate safety check watches it. Nothing is sent from this page.

Scenario clock: Tuesday, September 22

  1. 9:02 AMYour team

    Someone on your team adds Nadia Ferreira to the sheet. Status: Offer accepted.

  2. 9:15 AMThe automation

    Nadia: not hired yet. Status is "Offer accepted". Nothing is sent until it says Hired.

  3. 10:38 AMYour team

    Status changed to Hired and a welcome address added.

  4. 10:45 AMThe automation

    Nadia: waiting for an approved address. The address is in the sheet; it goes out once someone initials Approved by.

  5. 11:06 AMYour team

    KR initials Approved by.

  6. 11:15 AMThe automation

    Letter to Nadia accepted for sending, to nadia.ferreira@example.org.

  7. 11:30 AMThe automation

    Goes through the sheet again: the same welcome email is not sent again.

  8. 11:30 AMThe safety check

    All clear. The automation last went through the sheet at 11:30 AM.

What happens

One letter, the first time the automation goes through the sheet after the address is approved (1 in total).

What is not sent

Nothing while the status is Offer accepted, nothing before someone approves the address. The next check does not send the same welcome email again.

What the team does

Enters the new therapist in the hiring sheet, and initials Approved by once the address is right.

Setup, tests & limitsFor your technical advisor: the rules, what was actually run, the permissions, and what is not done.

The rules, as proposed

These are proposals to confirm with your team, not a description of how Mentella works today. This demo covers the welcome email only; it is not a proposal to keep the spreadsheet as your hiring system, and the same rules would move to whatever replaces it.

  • Ready to send: initials in Approved by approve the name, address and start date the automation sees the first time it goes through the sheet after they appear. If any of the three changes afterwards, nothing goes out until someone writes Approve new address in the Decisions tab, and the row is read again just before each letter. A change made before that first run cannot be told apart from what was approved; for a stronger rule, the approver would record the approved address itself.
  • “Hired”, “hired” and “Hired ” (with a space at the end) all count as Hired. “Offer accepted” is not Hired and never leads to a letter.
  • For this nonclinical welcome message, your team would confirm which address may be used, a personal one included.
  • Each arrival gets a stable Arrival ID, written once in the sheet by the routine. It is the arrival, not the row and not the person: copies and sorting change nothing, and a therapist who returns years later is a new arrival with a new letter. A number is never handed out twice, even after an interrupted write. If someone clears the reference of a row already welcomed, the row is held for review instead of getting a second letter (same address and start date). We would also protect the column in Sheets so only the automation edits it.
  • A withdrawal cancels what is still waiting. A letter already sent stays in the history.
  • A missing or impossible start date (“2026-10-32”) holds the letter for review.
  • Sent means the mail service accepted the letter. Apps Script returns no delivery receipt; bounces arrive in the sending account, which someone should read.

If a send may or may not have happened

Before calling the mail service, the routine writes “attempt” in the journal and commits it (SpreadsheetApp.flush); if that write fails, no letter is sent. After the call it writes “sent”. If the execution stops in between, the next run finds an attempt with no result. It does not guess: the arrival goes to Needs review, the check emails the technical owner, and no further send is attempted until someone reviews what happened. They look in the Sent folder and write Confirm sent, or Send again knowing the first letter may still have gone out, in the Decisions tab with their name in By. An unsigned decision, or both answers at once, is ignored. A lock keeps two runs from overlapping; it is not a transaction between the mail service and the sheet, which is why the journal carries the attempt.

The check

  • A second Apps Script project, installed by another account, with view-only access to the hiring sheet and the journal. The journal is a separate workbook the team does not edit (one project cannot read another’s script properties, so the journal is the shared record).
  • Every 15 minutes (a demonstration setting) it looks at when the last run finished and at the sheet’s columns, then at results: approved arrivals still waiting, Needs review, uncertain sends, and rows that still have no reference after two runs. A stop is reported at the first check after about 40 minutes without a finished run, not within 15 minutes.
  • A problem counts as notified only once the mail service accepted its note; if the note fails, the next check sends it again. The morning summary never uses up an alert. It says separately whether the automation is working and what is left for your team, and counts letters from the journal, so a deleted row does not hide a letter already sent. The check runs under its own lock.
  • If it cannot read the sheet or the journal, or a column is missing, it says check impossible, never “no problems”.
  • It cannot see an arrival that is in none of the sheets it reads. Two Google projects reduce shared failure; they do not protect against an outage of Google Workspace itself.

Going live, and living with it

  • First read in dry run: the routine records what it would send and sends nothing. Arrivals starting before a chosen date are recorded as history and never mailed.
  • installTriggers can be run again at any time; it keeps exactly one trigger, and replaces it if the cadence changed. verifyDeployment runs after every code change, as the operating account, and reports missing permissions, columns, the letter, the switch and the triggers.
  • A dedicated sending account, with replies going to a team inbox someone reads, rather than an alias of a personal mailbox. The code lives in its own project, not inside the sheet everyone edits.
  • Periodic reading rather than an edit trigger: an edit trigger does not fire for changes made by script executions or API requests, and a simple one cannot send mail. The daily summary arrives within its chosen hour, not at an exact minute.
  • The routine reads the remaining daily mail quota from Google once per run and counts down locally (a local budget, not a reservation); when it runs out, it stops before the next attempt and the next run resumes.

Handing it over

  • Change the letter: edit the Welcome letter tab. Each send records a fingerprint of the letter version, not a full copy of the message sent.
  • Pause sending: set Sending to Paused in the same tab. Letters wait; the check says so.
  • Handle an alert: every note names who acts and what to do; answers go in the Decisions tab.
  • Replace the technical owner: triggers belong to the account that installed them and are not shared. The new owner runs installTriggers and verifyDeployment under the operating account, and the old owner’s triggers are removed. The check notices if passes stop in between.

Health information

The people and details on this page are fictional; there is no health information in it. Google lists Apps Script among the services covered by its HIPAA Business Associate Agreement (list dated August 31, 2026); that covers the service under a signed agreement, not this code and not this page. Employment records a covered entity holds in its role as employer are outside the definition of protected health information (45 CFR 160.103), which depends on context. Third-party services do not inherit Google’s agreement; what matters is which service receives which data under which contract. The page asks search engines not to index it; that is not access control.

Three levels of proof

  1. Replayed in this browser. The arrivals and the four situations above run the decision code of the tests, on fictional rows, with a scenario clock.
  2. Run in Google Apps Script. With the first published version of this code (commit ac0d2c4, before the review fixes of the same day), on a test Google account of ours, in two separate Apps Script projects (the same account for both: the second identity was not tested), letters sent only to that account's own inbox, on September 25, 2026:The current version, which adds the fixes listed in the tests (notes retried until accepted, rows without a reference counted, approval snapshot, reread before each letter, references never reused, journal committed before the mail call, date and decision checks), has not been run again in Google: it is covered by the tests below, including the generated Apps Script files run against small fakes of Google's services.
    • First read, dry run: 3 past hires recorded as history, 0 letters sent, 1 letter listed as would-send.
    • Going live: 1 letter sent (Maya); it arrived in the test inbox with the name and start date merged.
    • Copy and sort: Maya's row copied and the sheet sorted: 2 rows, 1 arrival, 0 letters.
    • Interrupted after the send: Interrupted on purpose (an error thrown right after the mail service accepted Grace's letter, before the journal write). The letter did arrive. Next reading: 0 letters, Grace in Needs review as uncertain.
    • Column renamed: Pass stopped before sending: no column named "Welcome email".
    • Installing twice: First run created the trigger, second created none: one trigger.
    • The check: Reported the uncertain letter and emailed it once. After the sending trigger was deleted, it emailed "the sending routine has stopped" with since when, who and what to do, and did not repeat the first alert.
    • Permissions: With the read-only Sheets permission the check could not open the workbooks and said "check impossible"; verifyDeployment then flagged the missing authorization. With the full Sheets permission it read both workbooks.
  3. Not done. Not run in Apps Script: two readings at the same moment (tested in code only), the check under a second account, view-only sharing of the workbooks to that account (proposed, since both projects ran under one account), the morning summary schedule. No connection to Mentella’s Workspace, no validation of these rules with your team, no installation.

Tests

71 of 71 passing (September 25, 2026). 20 of them are negative controls: the same situation with one or more safeguards switched off, which must show the failure it prevents.

the generated Apps Script bundle (sandboxed fakes, not Google)

  • passedruns the documented setup tests with the expected results
  • passedrefuses to run the setup tests if any address is not the running account's own
  • passedthe checking project reads the same workbooks and reports a stopped routine
  • passedA01: a note that fails to send is sent at the next check
  • passedA02: the morning summary does not use up an alert meant for its recipient
  • passedA03: a lower-case header is found when writing the reference, as when reading
  • passedthe journal is flushed before the mail call; if the flush fails, no letter is sent

normal arrival

  • passedsends one letter to the approved address, with the name and start date merged
  • passeddoes not send before the team approves
  • passednegative control: without the approval rule, an unapproved arrival is sent
  • passedreads "hired " as Hired and never reads "Offer accepted" as Hired
  • passednegative control: without normalisation, "hired " is missed
  • passedholds a letter whose address changed after approval, until the team approves the new one

copied and sorted rows keep their identity

  • passeda copied row does not produce a second letter; sorting sends the right people
  • passednegative control: with identity by row number, sorting gives Maya a second letter and Daniel none
  • passednegative control: a copy is caught by two separate guards; only with both off is it sent twice

two passes at the same time

  • passedthe second pass waits for the first: one letter
  • passednegative control: without the exclusive pass, two letters

the execution stops after the mail service accepted the letter

  • passedmarks the letter as uncertain and never resends on its own
  • passed"Send again" from the team sends exactly one more letter
  • passednegative control: recording only after the send, the next pass sends it again

a required column goes missing

  • passedthe pass stops before sending and the check reports it cannot check
  • passednegative control: without the column check, nothing is sent and the check says all clear
  • passedan unreadable sheet is never reported as zero problems

the sending routine stops (its trigger is deleted)

  • passedthe check raises one alert saying since when, who acts and what to do
  • passednegative control: without the heartbeat check, all clear
  • passedan alert whose note was accepted is not sent again at the next check

go-live: the first read never mails the history

  • passeddry run records what it would send; live mode sends only arrivals from the chosen start date
  • passednegative control: without the go-live scope, thirty old hires get a welcome letter

installing twice

  • passedleaves exactly one trigger
  • passednegative control: without deduplication, two triggers
  • passedremoves extra copies left by an older installer

withdrawal

  • passedcancels a letter still waiting (here: daily mail limit reached) and keeps the history
  • passednegative control: without the cancellation, the withdrawn therapist is welcomed
  • passedafter the letter went out, a withdrawal changes nothing sent and is noted

rehire

  • passedis a new arrival with its own letter
  • passednegative control: deduplicating by person, the returning therapist gets no letter

the check looks at results, not only at the routine

  • passedan approved arrival still unsent after an hour is reported, with the reason where to look
  • passeda refused send is shown for review and not retried on its own
  • passedsending paused: letters wait, the check says so
  • passeda Decisions row that does not apply is recorded as ignored and never used later
  • passedan Arrival ID is written only if the row still holds the same person

A01/A02: an alert is never lost because its note failed

  • passedthe next check still has the alert to notify
  • passednegative control: counting first detection as notified, the alert is never sent again

B06: a row whose reference could not be written stays in the check

  • passedcounted at once, reported after two readings, never all clear
  • passednegative control: without counting, the check says all clear with the row in the sheet

B07: the summary separates the automation from work left for people

  • passedwith a person waiting for an address, it never says nothing needs a person
  • passedwitness: with nobody waiting, the same summary says there is nothing to do

B03: approval binds the name, the address and the start date

  • passeda change of name or date after approval holds the letter
  • passednegative control: without the snapshot, the letter goes out with the new details

B01: what an approval covers (documented behaviour)

  • passedinitials are bound to the values seen at the first reading after they appear

B02: the row is read again just before each letter

  • passeda withdrawal made during the reading stops the letter
  • passednegative control: without the second read, the letter goes out

B04: a cleared reference does not produce a second letter

  • passedthe row is held for review
  • passednegative control: without the hold, Maya gets a second letter
  • passeda rehire (new start date) is still welcomed

B05: a reference is never handed out twice

  • passedMaya and Daniel end up with different references
  • passednegative control: counting journal entries, Daniel gets Maya's reference

B08/B09: a missing or impossible start date holds the letter

  • passedmissing date: nothing sent, reported
  • passednegative control, missing date: without the check, the letter goes out with a wrong or empty date
  • passedimpossible date: nothing sent, reported
  • passednegative control, impossible date: without the check, the letter goes out with a wrong or empty date

B10/B11: decisions must be signed and must not contradict each other

  • passedB10: an unsigned Send again is ignored
  • passedB10 negative control: without the rule, the unsigned decision sends a second letter
  • passedB11: Send again and Confirm sent together are both ignored
  • passedB11 negative control: without the rule, both are recorded as applied

B12: the summary counts letters from the journal

  • passeda letter still counts after its row is deleted
  • passedwitness: with no letter in the journal, it says none

B13: a reading records when it finished, not only when it started

  • passedcompletedAt is the end of a slow reading, and the check uses it

B14: reinstalling with a new cadence replaces the trigger

  • passed15 then 5 minutes: the old trigger is removed and one new one created
  • passedwitness: same cadence, nothing changes

Permissions

  • Sending project: spreadsheets, send mail as the account, manage its own triggers. No access to read anyone’s mailbox.
  • Checking project: spreadsheets, send mail, manage its own triggers. Its read-only access comes from view-only sharing of the two workbooks: in our run, Apps Script refused to open a workbook by ID under the read-only Sheets permission, and the check reported “check impossible”, as it should.

Code

Demonstration code, configuration and documented setup tests. One decision core in TypeScript, compiled for this page and bundled for Apps Script.