# Slack — Workspace Token Cache

Keeps each Slack workspace's bot token and user ready for other workflows, refreshed from Tooling every five minutes.

| Platform | Workflow ID | Status | Version | Last saved | Nodes | Systems |
| --- | --- | --- | --- | --- | --- | --- |
| n8n | aqaoUgxhGVLTnKUR | Active | 565b640c-345d-4880-b4c7-4622c45b3d95 | 2026-11-06T19:41:23.849Z | 18 | Tooling and Access |

Slack — Workspace Token Cache keeps the Slack bot of each workspace ready for the workflows that post to Slack. It reads the bot accounts from Tooling and Access, keeps one row per Slack workspace in an n8n Data Table for 15 minutes, and answers other workflows with the bot token and bot user of the workspace they ask for. It refreshes every workspace every five minutes, so a caller usually gets an answer without reaching Airtable.

It writes to no business system. Its only output is the n8n Data Table `slack_workspace_token_cache`, which is storage inside n8n, and the answer it returns to the workflow that called it. The Tooling and Access maintainer owns the bot accounts it reads.

## 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["Triggers"]
    n2(["Every 5 Minutes"]):::trigger
    n11(["Refresh Manually"]):::trigger
    n9["Refresh All Workspaces"]:::logic
    n16(["Token Request"]):::trigger
    n4["Normalize Request"]:::logic
    n10{{"Refresh All?"}}:::decision
  end
  subgraph g1["Answer from the cache"]
    n8[("Read Workspace Cache<br/><small>get n8n Data Table slack_workspace_token_cache</small>")]:::read
    n1["Evaluate Cache"]:::logic
    n12{{"Refresh Needed?"}}:::decision
    n14["Return Cached Result"]:::logic
  end
  subgraph g2["Refresh from Airtable"]
    n7[("Read Cache for Refresh<br/><small>get n8n Data Table slack_workspace_token_cache</small>")]:::read
    n6[("Read Bot Accounts<br/><small>GET Tooling Tool Account</small>")]:::read
    n0["Build Cache Rows"]:::logic
    n3{{"Has Cache Rows?"}}:::decision
    n5[("Persist Workspace Tokens<br/><small>upsert n8n Data Table slack_workspace_token_cache</small>")]:::write
    n15["Return Refreshed Result"]:::logic
  end
  subgraph g3["Errors"]
    n13["Return Cache Error"]:::logic
  end
  n2 --> n9
  n9 --> n4
  n11 --> n9
  n16 --> n4
  n4 --> n10
  n4 -.->|error| n13
  n8 --> n1
  n8 -.->|error| n13
  n1 --> n12
  n1 -.->|error| n13
  n7 --> n6
  n7 -.->|error| n13
  n6 --> n0
  n6 -.->|error| n13
  n0 --> n3
  n0 -.->|error| n13
  n5 --> n15
  n5 -.->|error| n13
  n10 -->|true| n7
  n10 -->|false| n8
  n12 -->|true| n7
  n12 -->|false| n14
  n3 -->|true| n5
  n3 -->|false| n15
  n15 -.->|error| n13
  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 |
| --- | --- | --- | --- |
| Tooling and Access | Tool Account | Read Bot Accounts | Nothing |
| n8n (not a business system) | Data Table `slack_workspace_token_cache` | Read Workspace Cache, Read Cache for Refresh | Persist Workspace Tokens |

Read Bot Accounts is an HTTP Request node with the Airtable token credential "Airtable PAT (Fully Scoped)". Its filter and the fields it reads use field IDs, so renaming Kind, Access Token, External Identifier or Workspace (External Identifier) in Tooling does not break it. See [Tooling and Access automations](/systems/tooling/automations) for the Tool Account records.

Two workflows call it through Token Request and wait for the answer unless noted:

| Calling workflow | Calling node | Inputs sent |
| --- | --- | --- |
| [Daily Digest](/automations/daily-digest) | Get Cached Notification Bot | workspaceId; cacheOnly, forceRefresh and refreshAll false |
| [Reading Reply](/automations/reading-reply) | Get Cached Bot | workspaceId; cacheOnly true when the Slack request's route is `open`, false otherwise |
| [Reading Reply](/automations/reading-reply) | Refresh Workspace in Background | workspaceId; forceRefresh true; does not wait for the answer |

## Triggers

| Trigger | When | What it runs |
| --- | --- | --- |
| Every 5 Minutes | Every five minutes | Refresh All Workspaces, then the refresh path for every workspace |
| Refresh Manually | Someone runs it in the editor | The same as Every 5 Minutes |
| Token Request | Another workflow calls it | The path chosen by its inputs |

Normalize Request trims `workspaceId` and turns `cacheOnly`, `forceRefresh` and `refreshAll` into true only when the caller sent the boolean true. Without `refreshAll`, the workspace ID must look like a Slack team ID (`T` followed by capital letters and digits); otherwise it throws INVALID_WORKSPACE and the caller gets the error answer.

Refresh All? sends `refreshAll` to the refresh path and everything else to the cache path.

## Answer from the cache

Read Workspace Cache gets the Data Table rows whose `workspace_id` is the requested workspace. Evaluate Cache accepts the row only when there is exactly one, it has an `access_token`, it has no `cache_error`, and `expires_at` is more than five seconds away.

| Situation | Answer |
| --- | --- |
| Valid row and no `forceRefresh` | `ok` true, `workspaceId`, `botSecret`, `botUserId`, `expiresAt`, `source` cache. Return Cached Result hands it back |
| Two rows for the workspace | Error CACHE_DUPLICATE |
| No row | Error CACHE_MISS |
| Row with a `cache_error` | That error code |
| Row about to expire, or `forceRefresh` | Error CACHE_EXPIRED |

On an error, Refresh Needed? goes to the refresh path, unless the caller sent `cacheOnly`: then Return Cached Result returns the error right away, without Airtable. The contract note on the canvas reserves `cacheOnly` for Slack interactive clicks, which must be answered quickly.

## Refresh from Airtable

1. Read Cache for Refresh reads every row of the Data Table, even when only one workspace is refreshed.
2. Read Bot Accounts reads Tool Account rows whose Kind is bot, 100 per page, following every page, once per execution, with a 15-second timeout per request.
3. Build Cache Rows groups the bot accounts by each value of Workspace (External Identifier) that looks like a Slack team ID. For each workspace to refresh it builds one row, or an error code:

| Code | When |
| --- | --- |
| AMBIGUOUS_BOT | More than one bot account for the workspace |
| BOT_NOT_CONFIGURED | No bot account for the workspace |
| BOT_TOKEN_MISSING | Access Token does not start with the Slack bot token prefix |
| BOT_USER_MISSING | External Identifier is not a Slack user ID (`U` followed by capital letters and digits) |

The workspaces to refresh are the requested one, or, with `refreshAll`, every workspace found in Tool Account plus every workspace already in the Data Table. A workspace that is only in the Data Table gets BOT_NOT_CONFIGURED, which clears its token.

4. Has Cache Rows? skips the write when there is nothing to write (only possible with `refreshAll` and no workspace anywhere).
5. Persist Workspace Tokens upserts each row by `workspace_id`.
6. Return Refreshed Result answers the caller. After `refreshAll` it returns counts: `workspaces`, `ready` and `invalid`. For one workspace it returns the same answer as a valid cache hit with `source` refresh, or the row's error code, or CACHE_EXPIRED.

## Fields written

### Tool Account fields read (Tooling and Access)

| Field | Used for |
| --- | --- |
| Kind | Only rows with bot are read |
| Workspace (External Identifier) | The Slack workspace IDs the bot belongs to |
| Access Token | The bot token copied to the cache |
| External Identifier | The bot's Slack user ID |

### Data Table columns written (Persist Workspace Tokens)

| Column | Value |
| --- | --- |
| workspace_id | Slack workspace ID; the upsert key |
| access_token | Bot token, or empty when there is an error code |
| bot_user_id | Bot's Slack user ID, or empty when there is an error code |
| source_record_id | Record ID of the Tool Account row, or empty when none matched |
| cache_error | Empty, or one of the codes under [Refresh from Airtable](#refresh-from-airtable) |
| expires_at | Write time plus 15 minutes, in milliseconds; 0 when there is an error code |

## Nodes

The workflow has 18 nodes: 17 that run and one sticky note. For an If node, output 0 is true and output 1 is false. Nodes set to continue on an error output (Normalize Request, Read Workspace Cache, Evaluate Cache, Read Cache for Refresh, Read Bot Accounts, Build Cache Rows, Persist Workspace Tokens and Return Refreshed Result) send failures out of output 1, drawn as dashed arrows in the flow.

| Node | Type | What it does |
| --- | --- | --- |
| Every 5 Minutes | scheduleTrigger | Schedule trigger, every five minutes |
| Refresh Manually | manualTrigger | Manual trigger in the editor |
| Refresh All Workspaces | set | Sets refreshAll = true for both refresh triggers |
| Token Request | executeWorkflowTrigger | Called by another workflow with workspaceId, cacheOnly, forceRefresh, refreshAll |
| Normalize Request | code | Validates the workspace ID and normalizes the flags |
| Refresh All? | if | refreshAll or a single workspace |
| Read Workspace Cache | dataTable | Data Table rows for the workspace |
| Evaluate Cache | code | Returns the cached bot when valid; otherwise says why |
| Refresh Needed? | if | Refresh now or answer as is |
| Return Cached Result | noOp | Passes the answer back to the caller unchanged |
| Read Cache for Refresh | dataTable | Every row of the Data Table |
| Read Bot Accounts | httpRequest | Tool Account rows with Kind bot, all pages |
| Build Cache Rows | code | One cache row per workspace, or an error code |
| Has Cache Rows? | if | Anything to write or not |
| Persist Workspace Tokens | dataTable | Upserts the rows by workspace_id |
| Return Refreshed Result | code | Answers the caller after a refresh |
| Return Cache Error | code | Answers TOKEN_CACHE_UNAVAILABLE with the stage |
| Cache Contract | stickyNote | Contract note on the canvas; does not run |

## Connections

Return Refreshed Result has no outgoing connection on output 0: its item is the answer returned to the caller.

| From | Output | To |
| --- | --- | --- |
| Every 5 Minutes | 0 | Refresh All Workspaces |
| Refresh Manually | 0 | Refresh All Workspaces |
| Refresh All Workspaces | 0 | Normalize Request |
| Token Request | 0 | Normalize Request |
| Normalize Request | 0 | Refresh All? |
| Normalize Request | 1 | Return Cache Error |
| Refresh All? | 0 | Read Cache for Refresh |
| Refresh All? | 1 | Read Workspace Cache |
| Read Workspace Cache | 0 | Evaluate Cache |
| Read Workspace Cache | 1 | Return Cache Error |
| Evaluate Cache | 0 | Refresh Needed? |
| Evaluate Cache | 1 | Return Cache Error |
| Refresh Needed? | 0 | Read Cache for Refresh |
| Refresh Needed? | 1 | Return Cached Result |
| Read Cache for Refresh | 0 | Read Bot Accounts |
| Read Cache for Refresh | 1 | Return Cache Error |
| Read Bot Accounts | 0 | Build Cache Rows |
| Read Bot Accounts | 1 | Return Cache Error |
| Build Cache Rows | 0 | Has Cache Rows? |
| Build Cache Rows | 1 | Return Cache Error |
| Has Cache Rows? | 0 | Persist Workspace Tokens |
| Has Cache Rows? | 1 | Return Refreshed Result |
| Persist Workspace Tokens | 0 | Return Refreshed Result |
| Persist Workspace Tokens | 1 | Return Cache Error |
| Return Refreshed Result | 1 | Return Cache Error |

## Settings and error handling

| Setting | Value |
| --- | --- |
| Execution order | v1 |
| Time zone | America/Recife |
| Execution timeout | 60 seconds |
| Save successful executions | No |
| Save failed executions | No |
| Save manual executions | No |
| Save execution progress | No |
| Error workflow | None |
| Retry on fail | Off on every node |
| Caller policy | Workflows from the same owner |
| Available in MCP | Yes |

Every failure inside the flow goes to Return Cache Error, which answers `ok` false, error TOKEN_CACHE_UNAVAILABLE and `stage`: the name of the last node it finds executed, checked from the end of the refresh path backwards. Failures in the nodes with an error output become this answer instead of a failed execution; the caller decides what to do with it.

## Known gaps

These come from the node configuration. Executions are not saved, so n8n keeps no history of past runs to compare with.

| Gap | Effect |
| --- | --- |
| The answer carries the bot token in `botSecret` | Any workflow of the same owner that calls it receives the token in its own execution data |
| The Data Table stores the bot token in clear text in `access_token` | Anyone who can open the Data Table in n8n can read every cached token |
| No execution is saved and errors become a normal answer | A broken scheduled refresh leaves no trace in n8n; it shows up only as callers getting CACHE_EXPIRED or TOKEN_CACHE_UNAVAILABLE |
| `stage` in the error answer is the last executed node found, not the node that failed | It points to the right area, not always to the exact node |
| A one-workspace refresh reads the whole Data Table and every bot account | Each forced refresh costs as much as a full refresh in Airtable calls |
| Two refreshes can run at the same time (schedule and a caller) | Both upsert the same rows; the last write wins, with the same data |
| The cache never asks Slack whether a token still works | A token revoked in Slack keeps being served until someone changes Access Token in Tool Account |

## Recover a partial write

The Data Table can be rebuilt at any time, so recovery is a refresh.

| What happened | What to do |
| --- | --- |
| Callers get CACHE_EXPIRED or CACHE_MISS for a workspace that has a bot | Run Refresh Manually; if the error stays, check the workspace's Tool Account row |
| A workspace answers AMBIGUOUS_BOT | Keep one Tool Account with Kind bot for that workspace, then refresh |
| A workspace answers BOT_TOKEN_MISSING or BOT_USER_MISSING | Fix Access Token or External Identifier on its Tool Account, then refresh |
| A refresh stopped halfway | Run Refresh Manually; the upsert replaces every row |
| Two rows for one workspace (CACHE_DUPLICATE) | Delete one row in the Data Table, then refresh |

## 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).