# Aida Client Digest v1

- `playbook_id`: `aida-client-digest`
- `version`: `1`
- `sha256`: `eb737788f5152f003286b36858a4f063bec22f3f70a221c0a8d42de84be64046`
- `playbook_ref`: `aida-client-digest@1#eb737788f5152f003286b36858a4f063bec22f3f70a221c0a8d42de84be64046`
- immutable after install: yes

This is the CLIENT-VISIBLE post. It lands in the client's own Slack channel and
is read by the client, not by us.

## Audience and tone

Write like a colleague's status note: state the number, state whether it moved,
state what needs a human. Nothing else.

Never use internal status vocabulary in the post — no `NEEDS_REVIEW`, no
`churned`, no workspace ids, no slugs, no campaign ids. Never dump the
dashboard, never paste a raw campaign list, never explain our tooling. A client
reading the post should learn what happened to their outbound this week and
what, if anything, is waiting on a person.

## Target: exactly one channel per client

Post to exactly ONE channel per client — the `SHARED` row of that client's
channel set. Never the `REPLIES` row and never the `FOLLOWUPS` row: those are
registry data that answer "which channels belong to this client", not delivery
targets. Posting the same digest three times into a client's three channels is
noise that trains them to mute all three.

Iterate ACCOUNTS, not channels. One account, one `SHARED` channel, one post.

## Metric order

Exactly this order, every time:

1. **Contacts reached / sends** — the denominator. Every rate below is
   meaningless without it.
2. **Replies, SPLIT into positive versus the rest.** Conflating reply types is
   the most common benchmarking mistake, and positive reply rate is the single
   most useful outbound metric there is. Report both the total and the positive
   count, and the positive rate.
3. **Meetings booked.**
4. **Exceptions.**

## Window

Rolling last 7 days, compared against the prior 7 days. Never day-over-day: at
these volumes a single day's positive replies is frequently zero, so a daily
delta is noise reported as signal.

## Source fields

Read these from `admin_client_status` for the one client this run is for. These
names are authoritative — never invent, rename or derive a substitute field.

| Number | Field path (current window) | Prior window |
|---|---|---|
| contacts reached | `window7d.metrics.messagesSent` | `priorWindow7d.metrics.messagesSent` |
| replies (all) | `window7d.metrics.repliesReceived` | `priorWindow7d.metrics.repliesReceived` |
| positive replies | `window7d.metrics.positiveReplies` | `priorWindow7d.metrics.positiveReplies` |
| positive reply rate | `window7d.metrics.positiveReplyRate` | `priorWindow7d.metrics.positiveReplyRate` |
| meetings booked | `window7d.metrics.meetingsBooked` | `priorWindow7d.metrics.meetingsBooked` |

Exception sources:

- **Zero-send days:** `window7d.points[].sent` — a per-date point whose `sent`
  is `0` inside a window that was supposed to be sending.
- **Senders in a bad state:** `window7d.senders[].status === "Wrong credentials"`.
- **Paused campaigns and low remaining leads:** the campaign exception list
  returned alongside the windows.

`points` and `senders` are the tool's own names for the upstream
`chart.points` and `senderPerformance` blocks. Use the tool's names — that is
what the run can actually read.

## Reference bands

Context only. These are practitioner benchmarks, not promises, and must never
be stated to a client as a target we committed to.

- Reply rate: ~3.4% average; 3-6% broad B2B; 6-8% targeted; 5.5% top quartile.
- Positive share of replies: 15-50%.
- Meetings: 0.5-1.5% of sends.
- Reply-to-meeting: 15-30%.

## Exception rule

True exceptions lead; routine numbers batch. Open with anything that needs a
human, then give the numbers. A paused campaign, a sender in a bad state and a
zero-send day are each exceptions.

## Claim before composing

At the start of a digest run, call
`admin_client_channel_record_delivery{phase:"claim", channelId, businessDate}`.

- `channelId` is the `ClientAccountChannel` ROW id of the `SHARED` channel, not
  the Slack channel id.
- `businessDate` is
  `new Intl.DateTimeFormat("en-CA", { timeZone: "America/New_York" }).format(new Date())`.

On `claimed:false`, reply `[SILENT]` and stop. Another run already covered this
client today, and a same-day manual retrigger must not deliver a second digest.
Delivery cannot be observed inside the run — the gateway delivers after the
reply — so the claim is what makes one-per-business-date true.

## Delivery is the gateway's job; the run only composes

Each active client has its OWN cron job carrying
`deliver: "slack:<that client's SHARED channel id>"`. The run's REPLY *is* the
message: Hermes resolves that target (`cron/scheduler.py:1145-1240`,
`_parse_target_ref` accepts a raw `[CGD][A-Z0-9]{8,}` id) and delivers it from
the gateway process, which is the only holder of the Slack bot token.

So this run composes ONE client's digest and returns it as its final output —
nothing else. It never opens a Slack transport, never runs a CLI send, never
names a channel in the post, and never posts for more than the one client its
job is for.

## One delivery per day, no edits

A cron reply cannot be edited after delivery. One-per-business-date is enforced
by refusing the second claim, not by updating a message. Editing a delivered
digest is a deferred nicety, not a fallback.

## `[SILENT]`

When a client has nothing worth saying, call nothing further for that client.
When the whole run has nothing to say, the reply begins with `[SILENT]` so
Hermes suppresses delivery while still saving the output.

## Message shape

Stay far inside Slack's limits — 4,000 characters for best results, 40,000
hard. Use mrkdwn: `*bold*`, `_italic_`, `> quote`.

## Channel problems

Two outcomes, two different remedies, and neither is retried inside the run:

- `NEEDS_CHANNEL_FIX` carries a raw Slack code and is a HUMAN action.
  `not_in_channel` means invite the Sellable Admin bot; `channel_not_found`
  means the stored id is wrong; `is_archived` means demote the row.
- `DELIVERY_FAILED` is reported and left alone — the next scheduled run retries
  it on its own.

A retry loop against a one-message-per-second-per-channel limit is a retry
storm, not a fix.

## Hard prohibitions

Never send prospect outreach. Never launch, pause or modify a campaign. Never
move a client account's status. Never open a Slack transport of any kind.

```contract-json
{"playbookId":"aida-client-digest","version":1,"audience":"client_visible","target":{"channelType":"SHARED","forbiddenChannelTypes":["REPLIES","FOLLOWUPS"],"channelsPerClient":1,"iterate":"accounts"},"window":{"kind":"rolling_days","days":7,"comparison":"prior_7","forbidden":"day_over_day"},"metricOrder":["contactsReached","repliesSplitPositive","meetingsBooked","exceptions"],"sourceFields":{"contactsReached":["window7d.metrics.messagesSent","priorWindow7d.metrics.messagesSent"],"replies":["window7d.metrics.repliesReceived","priorWindow7d.metrics.repliesReceived"],"positiveReplies":["window7d.metrics.positiveReplies","priorWindow7d.metrics.positiveReplies"],"positiveReplyRate":["window7d.metrics.positiveReplyRate","priorWindow7d.metrics.positiveReplyRate"],"meetingsBooked":["window7d.metrics.meetingsBooked","priorWindow7d.metrics.meetingsBooked"],"zeroSendDays":["window7d.points[].sent"],"senderState":["window7d.senders[].status"],"campaignExceptions":["campaigns[]"]},"benchmarks":{"replyRateAverage":0.034,"replyRateBroadB2b":[0.03,0.06],"replyRateTargeted":[0.06,0.08],"replyRateTopQuartile":0.055,"positiveShareOfReplies":[0.15,0.5],"meetingsPerSend":[0.005,0.015],"replyToMeeting":[0.15,0.3],"status":"practitioner_benchmark_not_promise"},"exceptions":{"lead":true,"kinds":["paused_campaign","sender_wrong_credentials","zero_send_day","low_remaining_leads"],"senderBadStateValue":"Wrong credentials"},"claim":{"tool":"admin_client_channel_record_delivery","phase":"claim","channelIdIs":"client_account_channel_row_id","businessDateTimeZone":"America/New_York","businessDateFormat":"en-CA","onClaimedFalse":"[SILENT]"},"delivery":{"owner":"hermes_gateway","mechanism":"cron_job_deliver_target","deliverPrefix":"slack:","runOutput":"reply_only","agentSlackTransport":"none","perBusinessDate":1,"edits":"not_possible"},"silentMarker":"[SILENT]","limits":{"preferredChars":4000,"hardChars":40000,"format":"mrkdwn"},"channelFailure":{"NEEDS_CHANNEL_FIX":{"codes":["not_in_channel","channel_not_found","is_archived"],"remedy":"human_action","retryInRun":false},"DELIVERY_FAILED":{"remedy":"next_scheduled_run","retryInRun":false}},"prohibitions":["prospect_outreach","campaign_launch","campaign_pause","status_mutation","slack_transport","in_run_retry"]}
```
