# Daily Digest

Sends each accountable person a Slack DM with the KPI readings due today and logs each attempt in Communication.

| Platform | Workflow ID | Status | Version | Last saved | Nodes | Systems |
| --- | --- | --- | --- | --- | --- | --- |
| n8n | V8YNeLy4FbFiPvsM | Active | cf9165c2-706b-4bfb-8658-c62f1bb4dac5 | 2026-11-06T19:41:05.769Z | 36 | KPIs & OKRs, Communication, Tooling and Access |

Communication — Daily Digest asks each accountable person, in a Slack direct message, for the KPI readings due that day. Every morning it finds the Active Recurrence Configurations whose cadence is due, removes the readings already recorded, groups the rest by accountable membership, finds each person's Slack account in Tooling and Access, sends one checkbox digest per membership and logs each attempt as a Message in Communication. The answers come back through the separate workflow Communication — Reading Reply, which handles the checkboxes.

The KPIs & OKRs maintainer owns the schedules and the due logic; the Communication maintainer reviews the Message rows; the Tooling and Access maintainer keeps people bound to their Slack accounts.

## Flow

Generated from the n8n version in the box above: every box is a node and every arrow is a connection in n8n. Pink pills are triggers, yellow hexagons are If and Switch nodes with their outputs on the arrows, blue cylinders read (Airtable, Data Table, HTTP GET), green cylinders write, purple boxes call another workflow, and dashed arrows are error outputs.

```mermaid
flowchart TD
  subgraph g0["Start and schema"]
    n6(["Every day 09:00"]):::trigger
    n33(["Webhook"]):::trigger
    n34(["When clicking ‘Execute workflow’"]):::trigger
    n4["Config"]:::logic
    n0["Airtable Schema"]:::logic
    n20[("Meta (Operation)<br/><small>getSchema KPIs base</small>")]:::read
    n19[("Meta (Communication)<br/><small>getSchema Communication base</small>")]:::read
    n21[("Meta (Tooling)<br/><small>getSchema Tooling base</small>")]:::read
    n27["Resolve Schema"]:::logic
    n30{{"Schema OK"}}:::decision
  end
  subgraph g1["Due readings"]
    n17[("List Due Schedules<br/><small>search KPIs Recurrence Configuration</small>")]:::read
    n10[("Get Cadences<br/><small>search KPIs Cadence</small>")]:::read
    n3["Compute Due Items"]:::logic
    n16{{"Is OK"}}:::decision
    n1["Build Answered Filter"]:::logic
    n7[("Find Answered<br/><small>search KPIs Observation</small>")]:::read
    n14["Group by Recipient"]:::logic
    n15{{"Has Digests"}}:::decision
    n18["Log Nothing To Do"]:::logic
  end
  subgraph g2["Slack recipient"]
    n13[("Get Slack Accounts<br/><small>search Tooling Tool Account</small>")]:::read
    n11[("Get Identity Bindings<br/><small>search Tooling Identity Binding</small>")]:::read
    n12[("Get Persons (Tooling)<br/><small>search Tooling Person</small>")]:::read
    n29["Resolve Slack Recipient"]:::logic
    n25{{"Recipient Resolved?"}}:::decision
    n9["Get Cached Notification Bot<br/><small>Slack — Workspace Token Cache</small>"]:::subflow
    n28["Resolve Slack Account"]:::logic
  end
  subgraph g3["Send and log"]
    n2["Build Slack Blocks"]:::logic
    n32{{"User Found"}}:::decision
    n22{{"Not Dry Run"}}:::decision
    n23[("Open DM<br/><small>POST Slack conversations.open</small>")]:::write
    n24[("Post Digest<br/><small>POST Slack chat.postMessage</small>")]:::write
    n8[("Find Membership (Communication)<br/><small>search Communication Membership</small>")]:::read
    n31["Shape Message Row"]:::logic
    n5[("Create Message<br/><small>create Communication Message</small>")]:::write
    n26["Report"]:::logic
  end
  n6 --> n4
  n4 --> n0
  n0 --> n20
  n20 --> n19
  n19 --> n21
  n21 --> n27
  n27 --> n30
  n17 --> n10
  n10 --> n3
  n3 --> n16
  n1 --> n7
  n7 --> n14
  n14 --> n15
  n13 --> n11
  n11 --> n12
  n12 --> n29
  n2 --> n32
  n23 --> n24
  n24 --> n8
  n8 --> n31
  n31 --> n5
  n5 --> n26
  n30 -->|true| n17
  n30 -->|false| n18
  n16 -->|true| n1
  n16 -->|false| n18
  n15 -->|true| n13
  n15 -->|false| n18
  n32 -->|true| n22
  n32 -->|false| n8
  n22 -->|true| n23
  n22 -->|false| n8
  n34 --> n4
  n33 --> n4
  n29 --> n25
  n25 -->|true| n9
  n25 -->|false| n28
  n9 --> n28
  n28 --> n2
  classDef trigger fill:#fde7f3,stroke:#d6409f,color:#111
  classDef decision fill:#fff7e0,stroke:#c99a00,color:#111
  classDef read fill:#e8f1ff,stroke:#3b82f6,color:#111
  classDef write fill:#e7f8ee,stroke:#16a34a,color:#111
  classDef subflow fill:#f3e8ff,stroke:#9333ea,color:#111
  classDef wait fill:#f1f1f1,stroke:#888,color:#111
  classDef logic fill:#ffffff,stroke:#999,color:#111
```

## Systems it touches

| System | Table | Reads | Writes |
| --- | --- | --- | --- |
| KPIs & OKRs | Recurrence Configuration | List Due Schedules | Nothing |
| KPIs & OKRs | Cadence (mirror) | Get Cadences | Nothing |
| KPIs & OKRs | Observation | Find Answered | Nothing |
| Tooling and Access | Tool Account | Get Slack Accounts | Nothing |
| Tooling and Access | Identity Binding | Get Identity Bindings | Nothing |
| Tooling and Access | Person (mirror) | Get Persons (Tooling) | Nothing |
| Communication | Membership (mirror) | Find Membership (Communication) | Nothing |
| Communication | Message | Nothing | Create Message |

Meta (Operation), Meta (Communication) and Meta (Tooling) also read the table and field list of the three bases. The Message row links the Channel Slack DM and the Notification Rule Reading due today by record ID, and the tag kpis-okrs-daily-digest-n8n.

Outside Airtable, Get Cached Notification Bot calls the workflow Slack — Workspace Token Cache for the bot token of each recipient's workspace, and Open DM and Post Digest call the Slack Web API.

The Airtable nodes are native Airtable nodes with the credential Airtable PAT (Fully Scoped). They read and write by column name, but every name comes from Resolve Schema, which translates the field IDs kept in Airtable Schema into the current names on each run. Renaming a column does not break the flow; deleting one stops it at Schema OK.

See [Daily Digest](/automations/daily-digest) for the reading side in KPIs & OKRs and [Daily Digest delivery](/automations/daily-digest) for the Message rows in Communication.

## Triggers and schedule

Three triggers start the same run at Config:

| Trigger | When |
| --- | --- |
| Every day 09:00 | Cron `0 9 * * *` in the n8n instance's time zone; the workflow sets none. Message rows from scheduled runs are created at about 12:00 UTC |
| Webhook | A GET on a fixed path, with no authentication. It answers at once. `?runDate=YYYY-MM-DD` runs the digest for that date |
| When clicking ‘Execute workflow’ | Manual run from the n8n editor |

Config sets the run's constants:

| Setting | Value |
| --- | --- |
| timezone | America/Chicago, used for the weekday, the period key and the date label |
| dryRun | false |
| dmChannelRecordId | The Channel Slack DM |
| ruleRecordId | The Notification Rule Reading due today |
| biweeklyAnchorIsoWeekParity | 0 (biweekly cadences fire on even ISO weeks) |
| maxItemsPerDigest | 25 |
| workspaceByOrganization | Slack workspace per organization: Snack Prompt, Corp; Sons Design , LLC; Sons Of Liberty Coffee, LLC; Score Milk (Top Score), LLC; Nexa Roofing, LLC; REFS Labs, Inc. |
| defaultWorkspaceTeamId | The Snack Prompt, Corp workspace |
| runDate | `runDate` from the webhook query, otherwise today's date on the n8n clock |

## Schema check

Airtable Schema holds the base, table and field IDs the flow uses in KPIs & OKRs, Communication and Tooling and Access. Meta (Operation), Meta (Communication) and Meta (Tooling) read each base's schema, and Resolve Schema maps every ID to its current name. When a table or field is missing, Resolve Schema returns "Schema drift — not found in Airtable: …" and Schema OK sends the run to Log Nothing To Do, which ends it.

## Due readings

1. **Schedules.** List Due Schedules reads the Recurrence Configurations with Lifecycle Status* Active and a Cadence (Key). Get Cadences reads the whole Cadence (mirror) catalog.
2. **Due today.** Compute Due Items skips a schedule whose cadence key is not in the catalog, whose cadence is not Active, or that has no Accountable Membership*. It keeps the ones due on the run date:

   | Cadence key | Due when |
   | --- | --- |
   | daily | Every day |
   | daily-working-days | The weekday is in the cadence's By Day |
   | weekly | Monday |
   | biweekly | Monday of an even ISO week |
   | monthly | Day 1 |
   | quarterly | Day 1 of January, April, July and October |
   | semiannual | Day 1 of January and July |
   | annual | January 1 |
   | on-demand, or any other key | Never |

   Each due item gets a period key from the cadence's Period Key Format and an occurrence key `<Recurrence Configuration record ID>:<period key>`. Its recipients are the schedule's accountable memberships with their name, e-mail, Membership # and organization. The name comes from Accountable Display Name; when that is a bare record ID, it is taken from Accountable (Membership #), which reads "number - name - organization".
3. **Already answered.** Build Answered Filter builds Occurrence Key formulas in chunks of 40, and Find Answered reads those Observations. Group by Recipient drops a membership's item when an Observation has the same Occurrence Key, lists that membership in Observation About (Membership)*, and has Collection Status* empty or Recorded.
4. **One digest per membership.** Group by Recipient groups what is left by accountable membership, keeps the first 25 items and counts the rest as overflow. A recipient without a name is set aside as unreachable. Has Digests ends the run at Log Nothing To Do when nothing is left.

## Slack recipient

Get Slack Accounts reads the Tool Accounts whose Kind is human (Name, External Identifier, Kind and Workspace (External Identifier) only; never Access Token). Get Identity Bindings and Get Persons (Tooling) read both tables whole. For each digest, Resolve Slack Recipient:

1. finds the Person (mirror) whose Full name* equals the recipient's name, ignoring case and extra spaces;
2. keeps the Identity Bindings of that person whose Valid Through is empty or in the future, and their human Tool Accounts;
3. picks the account in the workspace mapped to the membership's organization, else the only account, else the account in the default workspace;
4. checks that the workspace ID starts with T and the Slack user ID with U.

Each miss becomes an error on the item ("Person not found in Tooling mirror: …", "No Slack account bound to …", "No Slack account for … in workspace …", INVALID_WORKSPACE, INVALID_SLACK_USER). Recipient Resolved? sends resolved items to Get Cached Notification Bot, which runs Slack — Workspace Token Cache with the workspace ID, `cacheOnly` false, `forceRefresh` false and `refreshAll` false, and waits for it. Resolve Slack Account accepts the answer only when it is ok, is for the same workspace, carries a bot token and bot user ID, and has not expired; otherwise the item gets TOKEN_CACHE_UNAVAILABLE, TOKEN_WORKSPACE_MISMATCH or TOKEN_CACHE_INVALID.

## Send and log

Build Slack Blocks writes the digest: a header "&lt;date&gt; · 0 of N responses recorded", then sections of up to 10 checkboxes with the action `reading_reply`, one per reading (name, description, goal), each with the value `<occurrence key>|<membership record ID>|<Slack user ID>`. Overflow adds the line "N additional readings are not shown in this message."

| Item state | Path |
| --- | --- |
| Error from routing or the token cache | User Found false → Find Membership (Communication) → Message Failed |
| Dry run | Not Dry Run false → Find Membership (Communication) → Message Sent with an External Message Id that starts with `dry-run-` |
| Ready | Open DM (`conversations.open`) → Post Digest (`chat.postMessage` with the blocks and the metadata event `reading_digest`) → Find Membership (Communication) → Message Sent or Failed from Slack's answer |

Open DM and Post Digest use the n8n credential Systems Slack Automation - Dynamic, which takes the bot token resolved for the item. Find Membership (Communication) looks up Membership (mirror) by Membership # (exact, or starting with "&lt;number&gt; - "). Shape Message Row builds the row and Create Message saves it. Report counts digests, sent and failed for the execution.

## Fields written

### Communication Message (Create Message)

| Field | Value |
| --- | --- |
| Subject | "Daily digest · &lt;person&gt; · &lt;run date&gt;" |
| Body | "&lt;date&gt;: N readings awaiting a response." |
| Status | Sent when Slack (or the dry run) answered ok, Failed otherwise |
| Sent At | Time of saving, when Sent |
| External Message Id | Slack message timestamp, or `dry-run-…`, when Sent |
| Failure Reason | The error, when Failed |
| Channel | Slack DM |
| Rule | Reading due today |
| Recipients (Membership) | The membership found by Find Membership (Communication), when found |
| About Keys | One occurrence key per reading in the digest, one per line |
| Tags | kpis-okrs-daily-digest-n8n |

The flow writes nothing else: no Observation, no Recurrence Configuration and nothing in Tooling and Access.

## Nodes

The workflow has 36 nodes: 35 that run and one sticky note. For an If node, output 0 is true and output 1 is false.

| Node | Type | What it does |
| --- | --- | --- |
| Every day 09:00 | scheduleTrigger | Starts the run at 09:00 instance time |
| Webhook | webhook | Starts the run from a GET; optional `runDate` |
| When clicking ‘Execute workflow’ | manualTrigger | Starts the run from the editor |
| Config | set | Run constants |
| Airtable Schema | code | Base, table and field IDs used by the flow |
| Meta (Operation) | airtable | Reads the KPIs & OKRs schema |
| Meta (Communication) | airtable | Reads the Communication schema |
| Meta (Tooling) | airtable | Reads the Tooling and Access schema |
| Resolve Schema | code | Maps field IDs to current names; reports drift |
| Schema OK | if | Schema resolved or not |
| List Due Schedules | airtable | Active Recurrence Configurations with a cadence key |
| Get Cadences | airtable | The Cadence (mirror) catalog |
| Compute Due Items | code | Due schedules, period keys, occurrence keys and recipients |
| Is OK | if | Schedules found or not |
| Build Answered Filter | code | Occurrence Key formulas in chunks of 40 |
| Find Answered | airtable | Observations for those occurrence keys |
| Group by Recipient | code | Drops answered items; one digest per membership, up to 25 items |
| Has Digests | if | Anything left to send or not |
| Log Nothing To Do | code | Ends a run with nothing to send |
| Get Slack Accounts | airtable | Human Tool Accounts |
| Get Identity Bindings | airtable | All Identity Bindings |
| Get Persons (Tooling) | airtable | All Person (mirror) rows |
| Resolve Slack Recipient | code | Person → binding → Slack account and workspace |
| Recipient Resolved? | if | Routing succeeded or not |
| Get Cached Notification Bot | executeWorkflow | Runs Slack — Workspace Token Cache for the workspace |
| Resolve Slack Account | code | Validates the cached bot token |
| Build Slack Blocks | code | Checkbox digest blocks and summary text |
| User Found | if | Item without error or not |
| Not Dry Run | if | Real send or dry run |
| Open DM | httpRequest | Opens the direct message in Slack |
| Post Digest | httpRequest | Posts the digest |
| Find Membership (Communication) | airtable | Communication membership by Membership # |
| Shape Message Row | code | Builds the Message row |
| Create Message | airtable | Saves the Message row |
| Report | code | Run summary |
| Sticky Note | stickyNote | Notes on the canvas; does not run |

## Connections

| From | Output | To |
| --- | --- | --- |
| Every day 09:00 | 0 | Config |
| Config | 0 | Airtable Schema |
| Airtable Schema | 0 | Meta (Operation) |
| Meta (Operation) | 0 | Meta (Communication) |
| Meta (Communication) | 0 | Meta (Tooling) |
| Meta (Tooling) | 0 | Resolve Schema |
| Resolve Schema | 0 | Schema OK |
| List Due Schedules | 0 | Get Cadences |
| Get Cadences | 0 | Compute Due Items |
| Compute Due Items | 0 | Is OK |
| Build Answered Filter | 0 | Find Answered |
| Find Answered | 0 | Group by Recipient |
| Group by Recipient | 0 | Has Digests |
| Get Slack Accounts | 0 | Get Identity Bindings |
| Get Identity Bindings | 0 | Get Persons (Tooling) |
| Get Persons (Tooling) | 0 | Resolve Slack Recipient |
| Build Slack Blocks | 0 | User Found |
| Open DM | 0 | Post Digest |
| Post Digest | 0 | Find Membership (Communication) |
| Find Membership (Communication) | 0 | Shape Message Row |
| Shape Message Row | 0 | Create Message |
| Create Message | 0 | Report |
| Schema OK | 0 | List Due Schedules |
| Schema OK | 1 | Log Nothing To Do |
| Is OK | 0 | Build Answered Filter |
| Is OK | 1 | Log Nothing To Do |
| Has Digests | 0 | Get Slack Accounts |
| Has Digests | 1 | Log Nothing To Do |
| User Found | 0 | Not Dry Run |
| User Found | 1 | Find Membership (Communication) |
| Not Dry Run | 0 | Open DM |
| Not Dry Run | 1 | Find Membership (Communication) |
| When clicking ‘Execute workflow’ | 0 | Config |
| Webhook | 0 | Config |
| Resolve Slack Recipient | 0 | Recipient Resolved? |
| Recipient Resolved? | 0 | Get Cached Notification Bot |
| Recipient Resolved? | 1 | Resolve Slack Account |
| Get Cached Notification Bot | 0 | Resolve Slack Account |
| Resolve Slack Account | 0 | Build Slack Blocks |

## Settings and error handling

| Setting | Value |
| --- | --- |
| Execution order | v1 |
| Error workflow | None |
| Retry on fail | Off on every node |
| On error | Continue on Get Cached Notification Bot, Open DM, Post Digest, Find Membership (Communication) and Create Message; stop the workflow on the others |
| Save successful executions | No |
| Save failed executions | No |
| Save manual executions | No |
| Save execution progress | No |
| Caller policy | Workflows from the same owner |
| Available in MCP | Yes |

Since 2026-11-06 n8n saves every execution of this workflow, successful, failed and manual, with progress. Before that date nothing was saved, so for older runs the Message rows are the only record.

## Known gaps

These come from the node configuration and from the Message rows the flow has written.

| Gap | Effect |
| --- | --- |
| The Webhook has no authentication | Anyone with its address starts a full real run for any `runDate` |
| The schedule runs at 09:00 instance time, while Config and the Notification Rule say America/Chicago | Runs land at about 12:00 UTC; the weekday and period key use the Chicago date, the run date uses the instance clock |
| On a day with nothing due, Build Answered Filter returns an error item that nothing checks | Find Answered runs with an empty formula and reads the whole Observation table; the run then ends saying everything is already answered |
| Open DM continues on error | Post Digest still runs, and the Message keeps `invalid_arguments` instead of the original Slack error such as `user_not_found` |
| Create Message continues on error | A digest can reach Slack with no Message row; since 2026-11-06 the n8n execution still shows it |
| Unreachable recipients (no name) are only counted in Report | They get no Message row; Report is visible only in the saved execution |
| A record ID can come through as the person's name | The digest is logged Failed with "Person not found in Tooling mirror: rec…"; three such rows were written on 2026-11-03 |
| The person is matched by full name only | Two people with the same name in Person (mirror) resolve to the first one; Valid From of a binding is ignored |
| Resolve Schema also requires Tool Account Access Token, which the flow never reads | Deleting that column stops every digest |
| No delivery idempotency | A second run on the same day sends the unanswered items again; a dry run still writes Message rows |
| The sticky note says the test restriction is on | Config has no testMembershipId or testScheduleId, so it is off; Build Slack Blocks still hard-codes one Slack user for it |
| A person with two memberships | Gets two digests |
| More than 25 items for one membership | The rest are counted, not sent |

## Recover a partial write

Look at the Message rows of the run before you start another one: a rerun sends again to everyone with something unanswered, and there is no rerun for a single recipient.

| What happened | What to do |
| --- | --- |
| Message Failed with a routing error | Fix the Person (mirror), Identity Binding or Tool Account in Tooling and Access; the next scheduled run tries again |
| Message Failed with a Slack or token error | Check the workspace's bot in Slack — Workspace Token Cache; the next scheduled run tries again |
| Digest in Slack but no Message row | Add the Message by hand from the Slack DM if the delivery must be counted |
| Run stopped at the schema check | Restore or remap the missing column, then wait for the next run or start one by hand knowing it sends to everyone |

## Keep this page in sync

The box at the top shows the n8n version this page describes. Open the workflow in n8n: when its last save is newer than the date in the box, this page is out of date until the next review. Every node and every connection of that version is listed in [Nodes](#nodes) and [Connections](#connections).