brick-activity
Contacts, what was said, and what happens next. People at business partners, and one record for every
call, email, meeting, note and task. A brick on erp-platform,
rebuilt from a general CRM activity model and held to the CRM spec (modules/crm). Every action is both
an API route and an MCP tool.
What it does
| Story | How |
|---|---|
| CRM-02 log a call or email against a contact in one step | name the contact and the company comes from them. If the rep has exactly one open deal there, the activity goes on that deal too; with several it says so instead of guessing. It shows on the contact, the company (everything logged against its people included) and the deal, newest first |
| CRM-12 my tasks as overdue, today and this week | a task is an activity with a due date; the board groups the caller's open ones (this week = the next seven days) |
| CRM-16, for the pipeline | a logged call, email or meeting on a deal — and a completed task on one — is reported to brick-pipeline (touch_opportunity), so a deal being worked is not flagged as gone quiet |
A task is an activity with a due date — one record, so a scheduled call and the call that happened are the same row, and completing a task keeps what it said. Something that happened cannot be in the future.
One email is one contact. DataK3 enforces nothing unique but a primary key, so the email is the key of
activity_contact_emails and a claim is a plain INSERT: a second contact with the same address fails at
the database (23505, verified live), even when two requests race. A contact who left the company is kept —
history points at them — and unsubscribed.
0.2.0 — the CRM audit's wave 1 (2026-10-17)
| Finding | Now |
|---|---|
| H5 a rep read another rep's deal through its timeline | a timeline filtered by opportunity_id — and a call logged onto a named deal — asks the pipeline as the caller whether they may see that deal: 404 when not, the pipeline's own answer. A contact or company that does not exist is 404, not an empty list |
| H12 six adds of one email at once: five 500s | the loser of the email claim (at the INSERT or at COMMIT) answers as if it came second — the same contact (or the "already … at partner …" 409). A NUL, a 300-character subject, due_on 2026-02-30, today=not-a-date, limit=-1 and a 23-digit bp_id are 422 |
| C3 two edits of one contact or task: one lost | contacts and tasks carry a version (GET /contacts/{id} sends it as the ETag); every change takes version or If-Match and answers 409 "changed since you opened it" when it is stale. Without one, a lost race is re-run on the latest |
| C4 | bp_id leaves as a string of digits; comes in as digits or a number |
| H13 a retried "log this call" made two | log_activity takes an Idempotency-Key header (MCP: idempotency_key): a repeat returns the first answer |
Who
Every signed-in user reads contacts and the activity record — the point of CRM-02 is that the next person
knows what was said. (Whether rows are owned at all is a customer_varies question in the spec; this brick
answers "shared".) Writing needs activity:contact:write / activity:activity:write; changing someone
else's contact or task needs to be its owner or activity:contact:manage / activity:activity:manage (0.3.0 — until
then the manager role name). A DEAL's activities are the exception (0.2.0): they
are shown to whoever may see the deal in the pipeline, and nobody else.
A brick decides by permission, never by role name (erp-platform 0.9.2). From 0.3.0 nothing here reads a role name. The roles are the customer's:
src/brick_activity/roles.yaml ships the defaults, from the CRM spec's actors; a composition maps them to its pool. The
process (application/process.yaml) is served as BPMN 2.0 at GET /process/bpmn.
Neighbours
| Brick | For | If not installed | If down |
|---|---|---|---|
| business-partner | a contact's company must exist; a company's timeline or contact list is of one that exists | taken as given | taken as given |
| pipeline | attach to the open deal; report activity; may the caller see a deal they named (asked as the caller) | no deal is attached unless named; a named one is taken as given | the activity is saved; the reply says the deal was not told. A deal's timeline, or logging onto a named deal, is 503 until it answers |
The activity brick's service identity needs pipeline:opportunity:touch. Across apps (tier 2) the pipeline
answers as that service and sees no rep's deals, so nothing is auto-attached there until the platform can
carry the acting person (the on-behalf-of gap in erp_platform.seam).
Tables
activity_contacts, activity_contact_emails (one email → one contact), activity_log (activities and
tasks), and (0.2.0) activity_contacts_versions / activity_log_versions, the optimistic-version claims. See
src/brick_activity/schema.sql. 0.2.0 migration (additive): a version column at the end of
activity_contacts and activity_log (ALTERs, DISRUPTIVE: reads stall for minutes) and the two claim tables —
migrate before rolling 0.2.0.
Run
pip install -e '.[test]' # needs erp-platform; brick-pipeline + brick-business-partner
pytest -q # installed too run the tier-1 tests
BUCKET=<scratch> DODIL_ORG=<org> python scripts/live_check.py
Not in 0.2
Importing contacts from a spreadsheet (CRM-13); syncing a mailbox or calendar; @mentions; private notes; merging duplicate contacts.
License: Apache-2.0, see LICENSE