# A do-not-contact reaching every channel

A do-not-contact directive is one memory row against the contact, carrying `block:<channel>` in its tag list. `directives()` reads only tags, which is why the refusal holds on every channel and in every future conversation rather than living in one channel's settings. Every attempt to reach that person afterwards ends in a refusal recorded with its reason, not in silence.

- **Status:** Available
- **Audience:** both
- **Channels:** email, whatsapp, sms, phone
- **In the app:** #/relationships, #/autonomy-audit, #/needs-you
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/workflows/dnc-across-channels/

## The chain

1. Trigger — somebody asks not to be contacted: by replying **STOP** to a text, by unsubscribing from an email, by saying so on a call, or by telling a person in the business who records it.
2. User or external event — the request arrives on one channel, and it is about all of them. Treating it as advice about one medium is the mistake this workflow exists to prevent.
3. Authentication and workspace resolution — the directive belongs to one workspace and to the contact inside it. Like every other record here it is scoped by the isolation kernel and by row-level security, and it does not travel to another workspace.
4. Ingest — for an inbound opt-out word, the scan happens before the message is treated as correspondence. For a request recorded by a person, the entry is created directly from the contact panel.
5. Canonical record — the conversation keeps the customer's own words where there are any. The evidence for a block is what makes it reviewable a year later.
6. Classification — the message is a directive, not a question. Nothing is drafted in reply and no approval is sought, because there is nothing to decide.
7. Knowledge, memory and rules — the directive itself: one `connect_memory` row against the contact, with the block in the `tags` list rather than in the body. Text in the body would be advice the engine could weigh; a tag is a fact it reads on every decision.
8. Autonomy — no mode overrides it. `autonomous` is permission to act without asking, never permission to contact somebody who has refused.
9. Action — nothing goes out. The check runs at the send boundary that every outbound message passes through, whether the engine wrote it, a person wrote it, a follow-up drain produced it or an outreach sequence scheduled it.
10. Result — a refusal per attempt, each recorded with the rule that caused it. Five refusals, one for each place contact could have been initiated: email, WhatsApp, SMS, phone and prospect outreach.
11. Relationship and timeline — the block is visible on the contact with its origin, so the next colleague to open the record sees why nothing is going out before they wonder.
12. Audit — the directive, and every refusal it causes, are in the Decision Log. A refusal is a decision and is recorded like one.
13. Next — the entry persists until a person with permission clears it. The Assistant cannot, at all.

## Why a tag rather than a column

Blocking is deliberately not a column on a contact. It is a memory row whose tag list carries the block, and `directives()` resolves it the same way every other memory tier is resolved — workspace, channel, endpoint, contact, narrowest first. Three consequences follow, and they are the reason for the design.

1. **It holds across channels.** A column on a channel's settings governs that channel. A directive against the contact is read wherever a decision about that person is made.
2. **It holds across future conversations.** Memory is not conversation history; it is resolved fresh on every piece of work, including work about a conversation that does not exist yet.
3. **It is one row, not five.** There is no per-channel copy to keep in step, and no risk that four channels honour a block while the fifth was missed.

> **Careful** The block must be in the tag list. A note in the body of a memory row saying not to contact somebody is not a directive — `directives()` reads only tags, so body text is context the model may weigh rather than a rule the send boundary enforces.

## The five refusals

| Where contact could start | What happens | What is recorded |
|---|---|---|
| An email reply or outreach step | Refused before the send | A refusal with its reason, against the thread |
| A WhatsApp reply or follow-up | Refused at the send boundary | The same, against the conversation |
| An SMS reply | Refused — and unavailable on the live carrier in any case | A refusal with its reason |
| A phone follow-up drain | The call is not placed | `refused: <reason>` against the follow-up |
| Prospect outreach and compliance screening | The person is screened out before an approach is drafted | The screening result, with the origin of the block |

Reading down that column is the honest test of whether a directive is worth the name. A block that stops one channel and lets an outreach sequence through has failed the person who asked, and the fact that it was technically honoured somewhere is no comfort to them.

## Setting and clearing one

1. Record the directive against the contact rather than the channel it arrived on, unless the person genuinely asked about one medium only.
   - Result: One entry covers every route to that person, now and later.
2. Leave the message that caused it in place.
   - Result: The evidence sits beside the block, which is what makes a review months later possible rather than speculative.
3. To clear one, read the origin first, then decide, then clear.
   - Result: The change is audited with your name. A channel preference and a withdrawal of consent look the same on a screen and are not the same thing.

> **Note** The Assistant may not clear a do-not-contact entry — one of the two explicit places its rights are narrower than a person's. Asking it to produces a refusal, which is itself recorded.

## Questions

### Does a do-not-contact entry stop the person contacting us?

No, and it should not. Their messages still arrive, are still attached to them and are still answerable by a person. Refusing to initiate contact is not refusing to listen — and somebody who writes after asking not to be written to is often asking for something specific.

### How is this different from a channel block?

Scope, and how hard it is to lift. A `block:sms` tag says not by text; a contact-wide directive says not at all, and is deliberately harder to clear because it records a withdrawal of consent rather than a preference about a medium.

### Will a new colleague accidentally message them?

A send by a person goes through the same boundary as a send by the engine, so the refusal applies to both. The block is also visible on the contact with its origin, which is usually where somebody stops before trying.

## Related

- [STOP and opt-out](https://connectbyjbrh.com/docs/sms/stop/)
- [SMS suppression](https://connectbyjbrh.com/docs/sms/sms-suppression/)
- [Memory in Connect](https://connectbyjbrh.com/docs/memory/)
- [An SMS STOP, end to end](https://connectbyjbrh.com/docs/workflows/sms-stop/)
- [Do not contact](https://connectbyjbrh.com/docs/email/dnc/)
- [Continuing on a different channel](https://connectbyjbrh.com/docs/workflows/cross-channel-handover/)

## What this page is based on

- `docs-source/sources/GENERAL.md` §3 and §6 — blocking as a tagged memory row, `directives()`
- `docs-source/sources/CHANNELS.md` §3 and §4 — STOP, suppression and the compliance check
- Connect capability registry (docs-source/facts.py) — `block_directive`, `suppression_dnc`
