# A spreadsheet view that is not a second database

`#/data` is an Excel-like grid over 13 sheets — companies, people, leads, prospects, follow-ups, deals, conversations, calls, cases, onboarding, knowledge, files and memory. It looks like direct table access and is not: every change goes through the service that owns the record, so business rules, scoping and the audit trail apply to a bulk edit exactly as they do to a screen.

- **Status:** Available
- **Audience:** both
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/research/grid-over-services/

## What the grid actually is

The grid is a view with spreadsheet ergonomics: virtualised rows, sort, filter, group, freeze, column resize and reorder, saved views, inline edit, bulk actions, duplicate detection, import and CSV export. People reach for it because a spreadsheet is the fastest way to see two hundred rows and change nine of them, and that is a real need rather than a shortcoming of the screens.

What it is not is a second copy of the data. There is no separate store behind it, no synchronisation job and no reconciliation. Each sheet is a presentation of records the same services own, so a value changed on the grid is the value the engine reads on its next tick.

## The one rule: writes go through the owner

| Edited on the grid | Owned by | What still applies |
|---|---|---|
| A person's lifecycle stage | `journey.py` | Stage vocabulary and progression rules, not an arbitrary string |
| A follow-up's date | `followups.py` | The reason requirement, and duplicate protection — same *when*, same person |
| A deal's value | `sales.py` | Values in minor units, stage rules on a move, review flags |
| A memory row | `connect_memory.py` | The tier that owns it, and tags — a `block:<channel>` directive is read from tags only |
| A thread's priority | The triage services | The engine reads `threads.priority` when it picks work, so the edit changes behaviour |

That is why some cells are not editable. A column the owning service derives is shown and not typed into, because there is no service call that would mean "set the derived value and leave the inputs". A grid that let you do it would be recording something the next recomputation deletes.

## What the constraint costs

- **A bulk action is not one statement.** Changing four hundred rows is four hundred service calls with rules, scoping and audit applied to each. It is slower than an `UPDATE`, and it is the only version that leaves a business in a state its own rules describe.
- **Some edits are refused rather than performed.** A stage move that breaks a stage rule fails on the grid the same way it fails on the pipeline screen.
- **Merging is proposed, not applied.** Duplicate detection surfaces candidates; the merge itself is a human decision, because it is close to irreversible and it re-parents identities, stages, follow-ups, deals, demos, cases and onboarding.
- **The grid cannot invent a column.** A sheet shows what the records hold. There is no free-form column to park a note in, and the place for that is memory or a field the record actually has.

## What it prevents

The failure this design avoids is not a crash; it is a second write path with different rules. Once one exists, half the follow-ups have reasons and half do not, some deals moved without passing their stage rules, and the audit trail answers "who changed this" for some rows and shrugs at others. Nothing alerts, and the two paths diverge for months.

Import is the same story at higher volume: rows arrive through the services rather than into the tables, so a spreadsheet somebody assembled over a weekend cannot introduce a record shape the rest of the system cannot read.

> **Note** CSV export neutralises formulas. A cell whose text begins like a formula is exported so a spreadsheet application will not execute it — otherwise a value typed by a stranger into a web form becomes code the moment a colleague opens the download.

## Questions

### Can hundreds of rows be changed at once?

Yes, through bulk actions and import. Each row still passes its owning service, so the operation is slower than a direct database write and every resulting record is one the rest of Connect can read. Rows that break a rule are reported rather than forced.

### Why can a column be read but not edited?

Because the owning service derives it. Writing a derived value would store something the next computation overwrites, and the grid deliberately does not offer an edit that cannot mean anything.

### Is an exported CSV safe to open?

Formula-shaped cells are neutralised on export, which addresses the case where text supplied by an outsider is executed by a spreadsheet application. Ordinary care with any downloaded file still applies.

## Related

- [Files and data in Connect](https://connectbyjbrh.com/docs/files-data/)
- [One file service for two audiences](https://connectbyjbrh.com/research/files-through-one-service/)
- [Why an agent's tools should call domain services](https://connectbyjbrh.com/research/agent-tools-call-services/)
- [Relationships in Connect](https://connectbyjbrh.com/docs/relationships/)
- [Separating the agent's authority from the person's](https://connectbyjbrh.com/research/authority-separation/)
- [Follow-ups in Connect](https://connectbyjbrh.com/docs/follow-ups/)

## What this page is based on

- `docs-source/sources/GENERAL.md` §9 — the grid, its sheets and the export rule
- `docs-source/sources/CHANNELS.md` §5 and §7 — `merge_people` as a human decision, and follow-up reasons and duplicates
- Connect capability registry (docs-source/facts.py) — `data_workspace`, `duplicate_merge`, `lifecycle_stage`
