I keep seeing automation briefs that ask for robust systems and explicit error
handling, while making a shared [Google Sheets](https://workspace.google.com/products/sheets/)
file responsible for the process's operational state.

That combination deserves a closer look.

My position is not that [Google Sheets](https://workspace.google.com/products/sheets/)
is bad, or that every small workflow needs [Supabase](https://supabase.com/).
Supabase is an application-backend platform built around a hosted
[PostgreSQL](https://www.postgresql.org/) database. In this note, it is a
familiar example of a controlled state owner: a place where the team can make
rules about identity, relationships, and changes part of the system instead of
hoping every user and workflow follows the same grid convention. A spreadsheet
can still be a useful place for a person to review input, correct a small batch,
or see the current picture.

The risk starts when the shared grid becomes the place the business relies on
to answer: “Did this happen?”, “What happens next?”, and “Who is allowed to
change the answer?” That is a risk decision, not a purity test about tools.

The builder should map the realistic failure modes, explain the cost and effort
of the available controls, and make a context-aware recommendation. The people
who own the commercial, operational, or security consequence decide what
remaining risk is acceptable.

> Alignment is sharing responsibility.

## Name the moving parts before choosing a tool

Imagine a small sales team preparing a customer-reengagement campaign. An
operator reviews a list of recipients in a spreadsheet. A workflow sends each
email. The sales lead checks the result before planning follow-up.

Before picking a tool, I would make the language explicit. That stops “row,”
“recipient,” and “sent” from quietly meaning different things to the operator,
the automation, and the next person asked to fix an incident.

| Concept | Type | Description |
|---|---|---|
| Spreadsheet | Working surface | The human-editable grid used to review campaign input or see results. It can be helpful without being the final authority. |
| Recipient record | Business object | One specific person in one campaign, such as Ada in the August re-engagement campaign. It is not the row where the person happens to appear today. |
| Primary key | Database identity | A value that uniquely identifies one record in its own database table, for example `campaign_recipient_id = rec_002`. The database rejects a second record with that same primary key. |
| Unique key | Duplicate-prevention rule | A value, or combination of values, that must not repeat. For example, `(campaign_id, recipient_email)` can prevent Ada from being added twice to the same campaign. It does not have to be the record's primary key. |
| Foreign key | Relationship rule | A value that must point to a real record somewhere else, such as a recipient's `campaign_id` pointing to an existing campaign. It prevents a recipient from belonging to a campaign that does not exist. |
| Source of truth | Authority | The component whose answer the team trusts when it asks whether `rec_002` was sent. Every other view should follow that answer rather than compete with it. |
| Workflow worker | Automation | The process that reads a recipient, asks an email provider to send, handles a retry, and records the outcome. |
| Side effect | Action outside the record | Something that changes the world beyond the local data: sending an email, creating a ticket, charging a card, or requesting fulfilment. |

With those roles clear, the recommendation becomes less ideological. The sheet
can remain a useful working surface while a more controlled component owns the
answer that matters.

## A row number tells you where something is, not who it is

Here is a deliberately small, hypothetical campaign list:

| recipient_id | customer_email | status | sent_at |
|---|---|---|---|
| rec_002 | ada@example.com | pending | |
| rec_003 | grace@example.com | pending | |

At 09:00, the worker reads row 2. It caches Ada's email address, asks the email
provider to send the campaign, and plans to write `sent` back to row 2.

While the request is in flight, a sales operator sorts the list by email or
inserts a newly approved recipient at the top. Row 2 can now contain Grace.
The worker may still send the right email to Ada because it cached her address,
then write `sent` against Grace because it remembered only the old row position.
The sales lead sees Grace as contacted and may skip her follow-up; Ada still
looks pending and may be sent the campaign again later.

```mermaid
sequenceDiagram
accTitle: A row can move while a workflow is running
accDescr: The worker reads Ada from row 2. An operator changes the list while the email request is in flight, and the worker records success against the old row position.
participant Worker
participant Sheet
participant Operator
participant Email provider
Worker->>Sheet: Read row 2 for Ada
Worker->>Email provider: Send to Ada's cached email
Operator->>Sheet: Sort list or insert a recipient
Worker->>Sheet: Write sent to the old row 2
```

A separate but related scenario creates duplicates. The email provider may
accept Ada's message, but the worker can lose the response because of a timeout
or transient network failure. If the workflow's only evidence is “I did not get
a clear response,” its next retry may send the same campaign again. The business
result is a duplicate email to Ada—not merely an inconvenient technical error.

Neither outcome is inevitable. The point is that a read-then-write design needs
to preserve identity deliberately; it cannot assume a mutable screen position
will remain the same business record.

The [Google Sheets API data model](https://developers.google.com/workspace/sheets/api/guides/concepts)
does provide stable identifiers for a spreadsheet and each sheet, plus tools
such as [named ranges and developer metadata](https://developers.google.com/workspace/sheets/api/guides/metadata).
Individual cells do not have unique IDs, though. A1 ranges, header names, and
row positions are therefore useful conventions, not an automatically enforced
primary key for a business record.

## What “atomic” actually means

Atomic means **all-or-nothing for one clearly named unit of work**. If the unit
is “store an attempt and mark `rec_002` as sent in one database transaction,”
either both database changes succeed or neither does. The record is not left
half-updated.

It does not mean that every step around that transaction becomes all-or-nothing.
This campaign workflow crosses separate systems:

```mermaid
flowchart LR
accTitle: The campaign workflow has separate steps
accDescr: The worker reads a recipient, asks an external email provider to send the message, then records the outcome. These steps are not one shared all-or-nothing operation.
ReadRecipient[Read recipient] --> SendEmail[Ask email provider to send]
SendEmail --> RecordOutcome[Record sent outcome]
```

| Unit of work | Is it atomic? | What that means in practice |
|---|---|---|
| One [Google Sheets `batchUpdate` request](https://developers.google.com/workspace/sheets/api/reference/rest/v4/spreadsheets/batchUpdate) | Yes, for the updates inside that one request | Google validates the request before applying its updates together. It does not include an earlier read or a later email request. |
| One transaction in a [Supabase](https://supabase.com/docs/guides/database/overview) database | Yes, for the database changes inside it | Supabase provides a hosted PostgreSQL database. A [transaction](https://www.postgresql.org/docs/current/tutorial-transactions.html) makes related local changes commit together or roll back together, keeping that database internally consistent. |
| Read recipient → send email → record result | No | It crosses the spreadsheet, the worker, and an external provider. A database cannot unsend an email that the provider has already accepted. |

That is why “the API has atomic updates” is useful but incomplete. The useful
beginner's question is: **which exact steps must either all happen or all not
happen, and which steps live outside that boundary?**

## Helpful spreadsheet rules are not the same as enforced business rules

A dropdown limiting a `status` cell to `pending` or `sent` is helpful. A
protected range can reduce accidental edits. Both are worth using when a
spreadsheet is the right working surface.

They are not automatically the same as a rule the state owner refuses to break.
In software design, that must-always-be-true rule is called an **invariant**.
For this campaign, a few useful invariants are straightforward in plain
language:

- one recipient must have one stable record identity;
- Ada must not appear twice in the same campaign by mistake;
- every recipient must belong to a real campaign;
- a record marked `sent` must have passed the team's defined confirmation point;
- a blank email address must not enter the send queue.

| If the sheet is the authority | If a controlled state owner is the authority |
|---|---|
| Two people can add Ada twice without a system-level duplicate check. | A **unique key** on `(campaign_id, recipient_email)` can reject the second record. |
| A row can point at a campaign name that was deleted or renamed. | A **foreign key** can require the referenced campaign record to exist. |
| `status` can be `sent`, `Sent`, `done`, or a typo, depending on how people and automations interpret it. | The state owner can define which values and transitions it accepts. |
| A person can set `sent = TRUE` before the email provider has confirmed the team's chosen success condition. | The worker can record an attempt first and change the status only at that confirmation point. |
| A blank address can be discovered after the workflow has already picked up the row. | Required values can be checked before the item becomes eligible to send. |

[Google Sheets data validation and protected ranges](https://developers.google.com/workspace/sheets/api/reference/rest/v4/spreadsheets/request)
are useful user-interface controls. A [Supabase database](https://supabase.com/docs/guides/database/overview)
runs on [PostgreSQL](https://www.postgresql.org/docs/current/), whose
[constraints](https://www.postgresql.org/docs/current/ddl-constraints.html)
provide a different kind of control: the database can reject an invalid primary
key, unique key, foreign-key relationship, or required value at the point of
write.

A database does not make every workflow safe by magic. It does give the team a
place to declare and consistently enforce the local rules that matter, instead
of asking every operator and automation to interpret the grid in the same way.

## A timeout is not an answer to “was the email sent?”

[Google Sheets API guidance](https://developers.google.com/workspace/sheets/api/troubleshoot-api-errors)
documents errors such as `500` and `503`, and its [usage limits](https://developers.google.com/workspace/sheets/api/limits)
document quotas and retry guidance. That does not mean every integration fails
often. It means a production workflow needs a clear answer for what to do when
a request fails or its outcome is unclear.

Return to Ada. The worker marks `rec_002` as `sending`, sends the request, and
the response times out. The technical response is “unknown.” The business
question is still open: did Ada receive the campaign or not?

A **state machine** is simply the small set of allowed answers to “where is
this item in the process?” Here, it stops a timeout being silently treated as
either success or failure.

```mermaid
flowchart LR
accTitle: A campaign message needs an explicit unknown outcome
accDescr: A pending recipient begins sending. The workflow can confirm a send, schedule a retryable problem, or mark the outcome unknown when it loses the response. An unknown outcome is checked before another send is attempted.
Pending --> Sending
Sending --> Sent
Sending --> RetryableProblem[Retryable problem]
Sending --> OutcomeUnknown[Outcome unknown]
OutcomeUnknown --> CheckEvidence[Check provider and attempt record]
CheckEvidence --> Sent
CheckEvidence --> Pending
```

**Idempotency** is the ability to repeat the same logical request without
creating a second effect. In this example, the logical request is “send the
August campaign to `rec_002`.” If the email provider supports it, the workflow
can give every retry the same idempotency key, such as
`august-campaign:rec_002`. The provider can then recognise that both attempts
refer to one intended message rather than send two.

The workflow still needs a durable attempt record and a way to check an unknown
outcome. Waiting longer before a retry—often called exponential backoff—changes
timing. It does not tell the worker whether the first request already succeeded.

## When being able to reconstruct the story matters

Not every small business process needs a compliance-grade history. For a
one-time event reminder, an operator may reasonably check a sent-email list and
correct the occasional mistake by hand.

The bar changes when the team may need to explain why a customer was contacted,
why a fulfilment request was repeated, who approved a change, or how a privacy
incident was contained. In that situation, an **audit trail** means a deliberate,
queryable history of business decisions and actions—not just a record of which
cells looked different later.

[Google Sheets version history](https://support.google.com/docs/answer/190843)
is useful for restoring a document and investigating visible human changes. It
also has documented boundaries: some changes may not appear in cell edit
history, and file owners can delete version history.

| If the team needs… | A spreadsheet history may be enough when… | A deliberate audit trail becomes useful when… |
|---|---|---|
| Recovery | An operator can restore a mistaken edit and manually re-run a small batch. | The team must tie a recovery decision to one specific recipient, attempt, and actor. |
| Investigation | It is enough to see that a document changed. | The team must answer who triggered an external action, what evidence they used, and what happened next. |
| Accountability | A local correction is cheap and low-consequence. | Money, customer trust, sensitive data, contractual commitments, or a repeatable operating process are involved. |

This is not a reason to burden a low-risk workflow with enterprise machinery.
It is a reason to make the history requirement explicit before a serious incident
makes it expensive to reconstruct.

## When this pattern fits: a spreadsheet is a good choice

I would keep [Google Sheets](https://workspace.google.com/products/sheets/)
in the design when most of these conditions are true:

- a person needs to review or edit a small, bounded input list;
- one controlled worker processes the list, or there is a clear processing window;
- a delay or occasional manual correction is acceptable;
- the external action is reversible, low-consequence, or easy to reconcile; and
- the sheet is an input or review surface, not the only evidence of what happened.

**Hypothetical lower-risk variant:** an operator reviews a one-time CSV export
for event reminders. Before sending starts, the worker takes a fixed snapshot:
the input stops changing for this run. Each row carries an immutable input ID,
meaning an ID that stays attached to the same recipient even if someone later
sorts a spreadsheet copy.

```mermaid
flowchart LR
accTitle: A lower-risk spreadsheet-backed batch
accDescr: An operator reviews a fixed CSV snapshot. One worker processes each item in order, appends an attempt result using the stable input ID, and asks for review when an outcome is unknown.
ReviewedInput[Reviewed CSV snapshot] --> OneWorker[One worker processes items in order]
OneWorker --> AppendResult[Append result using input ID]
AppendResult --> ReviewUnknown[Review unknown outcomes]
```

This does not promise exactly-once email delivery. It makes the remaining
uncertainty visible: the source input does not move while it is processed, the
result is attached to a stable item ID instead of a row coordinate, and an
unclear outcome is reviewed rather than hidden behind `sent = TRUE`.

## The recommendation should be a risk decision, not a tool verdict

There is no honest row-count threshold at which a spreadsheet suddenly becomes
a database. Consequence and control matter more than volume.

| Question to work through together | Example of a lower-risk answer | Example of a higher-risk answer |
|---|---|---|
| What is the process trying to achieve? | Send a reviewed, one-time reminder batch. | Keep an ongoing order, inventory, or customer lifecycle accurate. |
| What happens if one record is wrong? | A delay or manual correction is acceptable. | Money, privacy, fulfilment, customer trust, or a contractual obligation may be affected. |
| Who can change the state? | One controlled worker after a short review window. | Several operators, forms, automations, or outside systems. |
| Which rules must always hold? | A visible list and human check are enough. | Duplicate prevention, real relationships, required values, and controlled transitions must be enforced. |
| What can a retry do? | Re-running is harmless or simple to check. | It may duplicate a charge, message, fulfilment, or ticket. |
| What does the team need to prove later? | A snapshot and a manual correction path are enough. | Each decision, actor, attempt, and outcome needs a reliable history. |
| What can the team afford now? | A simple process plus a documented manual check is proportionate. | The cost of an incident outweighs the extra work of a controlled state owner. |

The builder's responsibility is to turn those answers into an honest
recommendation: name the plausible consequence, show the control that reduces
it, explain what that control costs, and make the remaining risk visible. The
executive or process owner then decides whether that residual risk is acceptable.

That alignment is not a hand-off of blame. It is the point at which the people
building and operating the system share responsibility for a conscious decision.

## When the spreadsheet should stop being the authority

I would move authoritative state to an existing business system or a database
when the process depends on several of these properties:

- multiple people or workers can change the same answer at once;
- a record needs a durable identity that is independent of its visible row;
- unique, primary-key, foreign-key, or state-transition rules need consistent enforcement;
- a retry can create a duplicate charge, message, fulfilment, or ticket;
- a wrong value can affect money, privacy, customer trust, or compliance; or
- the team needs a dependable way to recover from partial success across systems.

The destination does not always have to be a new [Supabase](https://supabase.com/)
project. If a controlled store is appropriate and no existing system owns the
concept, Supabase is one familiar option—not a default answer.
If an order system already owns orders, let it own order state. If a CRM owns a
customer lifecycle, do not create a second ledger in [Google Sheets](https://workspace.google.com/products/sheets/).
The principle is to give authority to the component that already owns the
business concept, or create a deliberately owned store when none exists.

The spreadsheet can still be valuable as an operator-friendly input, review
screen, or export. The risky design is letting that helpful view become a
second write path that other systems silently trust.

## Common questions

### Can a spreadsheet be the right source of truth?

Yes—when the process is small, controlled, reversible, and the people who own
it understand the cost of a mistake. The important decision is not whether a
[Google Sheets](https://workspace.google.com/products/sheets/)
file exists; it is whether the process needs stronger identity, duplicate
prevention, control over simultaneous changes, or recovery than the team has deliberately
built around it.

### Does moving to a database make a workflow exactly-once?

No. A transaction in a [Supabase](https://supabase.com/docs/guides/database/overview)
database can make its own database changes all-or-nothing. It cannot
automatically undo an email, payment, or ticket that an external provider has
already accepted. The workflow still needs clear states, idempotency where
supported, and a way to reconcile an unknown result.

### Can the spreadsheet remain the operator interface?

Often, yes. Let people use the sheet to review input or see a current result,
but route the authoritative change through the system that owns the business
state. If operators need to make that change, design that action deliberately
instead of treating a shared [Google Sheets](https://workspace.google.com/products/sheets/)
file as an accidental API.

## The takeaway

The useful question is not “Can this be built with [Google Sheets](https://workspace.google.com/products/sheets/)?”
It is “What is the cost of the wrong answer, what control is proportionate, and
who has agreed to the remaining risk?”

- Keep the spreadsheet when its convenience matches the consequence of being wrong.
- Move authority when the process needs guarantees the grid does not own by default.
- Make the trade-off visible, agree it with the people who own the outcome, and
  document what happens when reality does not follow the happy path.

If a spreadsheet has quietly become the authority for an operational process
you need to make safer, [message me](/#contact) and tell me
where the current hand-off breaks.