Series: Building Quorentra CRM from Zero — A Modular, ChatGPT-Native AI CRM
Building conversational CRM activity capture for calls, emails, meetings, and notes with entity resolution, confirmation, audit, and customer timelines.

1. Introduction
Quorentra now understands the three core commercial entities of a CRM:
CompanyContactOpportunity
In Part 20, we built conversational Company creation.
In Part 21, we added Contact creation and established:
Company
/ \
/ \
▼ ▼
Contact Opportunity
This tells Quorentra:
Who is the customer?Who are we talking to?What are we trying to sell?
But it still cannot answer an equally important question:
What happened?
Suppose we have:
Company:Adventure WorksContact:Sarah JohnsonOpportunity:Azure MigrationValue:€90,000
Then the user says:
Record that I called Sarah Johnson at Adventure Works today about the Azure migration. She asked for revised pricing.
That interaction contains valuable CRM information.
We know:
Activity Type:CallContact:Sarah JohnsonCompany:Adventure WorksOpportunity:Azure MigrationDate:TodaySummary:Discussed the Azure migration.Customer requested revised pricing.
That information should become part of the permanent customer history.
In Part 22, we build the first version of Quorentra’s:
CRM Activity Timeline
This gives Quorentra the beginnings of reliable organizational memory.
2. The Target Experience
The user says:
Record that I called Sarah Johnson at Adventure Works today about the Azure migration. She asked for revised pricing.
ChatGPT extracts the user’s explicit facts.
Quorentra resolves the referenced entities.
Then the user sees:
┌──────────────────────────────────────────────┐│ QUORENTRA ││ Record Activity ││ ││ Call ││ ││ Sarah Johnson ││ Adventure Works ││ ││ Opportunity ││ Azure Migration ││ ││ Date ││ 2 August 2026 ││ ││ Summary ││ Discussed the Azure migration. ││ Customer requested revised pricing. ││ ││ This will add an activity to the CRM. ││ ││ [Cancel] [Record Activity] │└──────────────────────────────────────────────┘
Only after explicit confirmation does the Activity become authoritative CRM data.
3. Why Activities Matter
CRM value does not come only from static records.
The real customer relationship consists of events:
CallEmailMeetingNoteProposalFollow-upDecisionStatus Change
Over time, those events form a timeline.
For example:
Adventure Works│├── 15 Jul — Contact created: Sarah Johnson│├── 20 Jul — Call: Discussed Azure migration│├── 23 Jul — Email: Sent technical proposal│├── 28 Jul — Meeting: Architecture workshop│├── 02 Aug — Call: Customer requested revised pricing│└── ...
That timeline becomes the memory of the commercial relationship.
4. Activities Unlock Better Questions
Once Activities exist, ChatGPT can eventually answer:
What happened with Adventure Works recently?
When did we last speak with Sarah Johnson?
What did the customer say about pricing?
Which opportunities have had no activity this month?
Summarize the history of the Azure migration opportunity.
What should I follow up on?
These capabilities depend on reliable historical data.
Therefore we should build the Activity layer before adding sophisticated CRM intelligence.
5. Keep the First Activity Module Small
It would be easy to expand this immediately into:
Microsoft Outlook synchronizationGmail synchronizationTeams meetingsGoogle CalendarZoomMeeting transcriptionCall recordingAutomatic email captureSlackWhatsAppTelephony
Do not do that yet.
The modular strategy remains:
Build the smallest useful capability, make it reliable, then extend it.
For the MVP, support four Activity types:
CallEmailMeetingNote
That is enough to establish the timeline architecture.
6. Starting Checkpoint
Before Part 22, verify Part 21.
From:
backend
run:
python -m pytest
Then:
cd ..\chatgpt-uinpm run build
Verify:
Add Sarah Johnson at Adventure Works. She’s the IT Director and her email is sarah.johnson@adventure-works.com.
Quorentra should:
Resolve Company ↓Detect Duplicates ↓Create Contact Draft ↓Prepare ↓Confirm ↓Create Contact ↓Audit
We will use Sarah Johnson throughout Part 22.
7. Define the Activity Permission
Introduce:
activities.create
Our permission model now includes:
Companies├── companies.read├── companies.create└── companies.updateContacts├── contacts.read├── contacts.create└── contacts.updateOpportunities├── opportunities.read├── opportunities.create└── opportunities.updateActivities├── activities.read└── activities.create
Later we can add:
activities.updateactivities.delete
For now, read and create are sufficient.
8. Example Role Matrix
For the MVP:
| Role | Read Activities | Create Activities |
|---|---|---|
| Viewer | ✓ | — |
| Member | ✓ | ✓ |
| Manager | ✓ | ✓ |
| Admin | ✓ | ✓ |
| Owner | ✓ | ✓ |
Again, the exact role names are less important than capability separation.
9. Define Activity Types
Create an enum:
class ActivityType(str, Enum): CALL = "call" EMAIL = "email" MEETING = "meeting" NOTE = "note"
This is deliberately small.
Later we may add:
TASK_COMPLETEDPROPOSAL_SENTDOCUMENT_SHAREDDEMOWORKSHOPSTATUS_CHANGESYSTEM_EVENT
But not yet.
10. Why Use Explicit Activity Types?
Because type enables future queries.
For example:
Show me all calls with Adventure Works.
or:
When was our last meeting with Sarah?
or:
How many customer meetings happened this month?
Without structured type information, these questions become much harder.
11. Define the Activity Domain
Our initial Activity model might contain:
idorganization_idactivity_typesubjectsummaryoccurred_atcompany_idcontact_idopportunity_idcreated_by_user_idversioncreated_atupdated_at
This is intentionally compact.
12. Relationships Are Optional
Unlike Contact creation, an Activity does not necessarily require every relationship.
For example:
Add a note to Adventure Works: strategic account for 2027.
This requires:
company_id
but not:
contact_idopportunity_id
Another example:
Record a call with Sarah Johnson.
If Sarah resolves uniquely, her Company can often be derived.
13. Minimum Activity Requirements
For the MVP, require:
activity_typeoccurred_atsummary
and at least one relationship:
company_idORcontact_idORopportunity_id
This prevents orphaned timeline entries.
14. Why Require a Relationship?
An Activity without any CRM relationship is difficult to use.
For example:
Call about pricing.
Who was it with?
Which customer?
Which Opportunity?
A CRM Activity should belong somewhere in the relationship graph.
15. Subject Versus Summary
We can support both:
subjectsummary
For example:
Subject:Azure migration pricing discussionSummary:Customer requested revised pricing.
But subject can be optional.
If not explicitly provided, the server may derive a simple display subject from Activity type and linked entities.
Do not require ChatGPT to invent elaborate titles.
16. Example Derived Subject
For:
CallSarah JohnsonAzure Migration
the application might derive:
Call with Sarah Johnson
or:
Azure Migration call
according to a deterministic display rule.
The user’s actual summary remains separate.
17. Field Ownership
Classify Activity fields.
| Field | Source |
|---|---|
id | System |
organization_id | TenantContext |
activity_type | User intent |
subject | User or server-derived |
summary | User |
occurred_at | User context / server-resolved |
company_id | Entity resolution |
contact_id | Entity resolution |
opportunity_id | Entity resolution |
created_by_user_id | Authenticated user |
version | System |
created_at | System |
updated_at | System |
Again:
ChatGPT expresses meaning. Quorentra resolves authoritative identity.
18. Never Accept Tenant ID
The Activity tool must never accept:
organization_id
from ChatGPT.
Tenant scope comes from:
TenantContext
This remains one of Quorentra’s core security invariants.
19. Never Trust User IDs from ChatGPT
Likewise:
created_by_user_id
comes from the authenticated principal.
ChatGPT must not be able to claim:
created_by_user_id = another_user
20. Introduce ActivityDraft
Reuse our draft architecture.
Conceptually:
ActivityDraft├── id├── organization_id├── user_id│├── activity_type├── subject├── summary├── occurred_at│├── company_id├── company_reference│├── contact_id├── contact_reference│├── opportunity_id├── opportunity_reference│├── status├── field_sources├── dependency_status├── workflow_id├── expires_at├── created_at└── updated_at
The ActivityDraft is temporary conversational state.
21. Why Use a Draft?
Consider:
Record that I called Sarah today.
Quorentra may resolve:
Activity Type:CallContact:SarahDate:Today
but perhaps multiple Sarahs exist.
The Activity should not be created until the identity is resolved.
The draft lets us preserve everything else while clarification occurs.
22. Draft Status
Reuse:
incompletecompletepreparedcancelledexpiredconverted
No new lifecycle is needed.
This is exactly why we built generic mutation infrastructure.
23. Date and Time Matter
Activities are historical events.
Therefore:
occurred_at
is essential.
The user might say:
todayyesterdaythis morningFridaylast Tuesday2 August at 14:00
These expressions need resolution.
24. Relative Date Resolution
Suppose today is:
2 August 2026
and the user says:
I called Sarah yesterday.
The application should resolve:
occurred_at =1 August 2026
using the user’s applicable timezone.
The final structured timestamp should be authoritative.
25. Do Not Store “Yesterday”
Never persist:
occurred_at = "yesterday"
Relative time is conversation-dependent.
Resolve it into a real timestamp before authoritative creation.
26. Timezone Strategy
A proper timestamp should eventually use:
UTC storage+user/organization timezone
For example:
User:Europe/AmsterdamInput:today at 14:00Stored:2026-08-02T12:00:00Z
depending on daylight-saving rules.
This is a server responsibility.
27. What If No Time Is Given?
User:
I called Sarah today.
We know the date, but not the exact time.
For the MVP, define an explicit policy.
One option:
occurred_at =current time
when the user says “today” without a time.
Another option is to model date precision separately.
For the first MVP, using current time is simpler, but record that the time was server-resolved rather than explicitly supplied.
28. Field Provenance Helps Again
Example:
{ "field_sources": { "activity_type": "user", "summary": "user", "occurred_at": "server_resolved_relative_time", "contact_id": "server_entity_resolution", "company_id": "server_derived_relationship", "opportunity_id": "server_entity_resolution" }}
This tells us how the Activity was constructed.
29. Call Activity
Example:
Record that I called Sarah Johnson today about the Azure migration.
Extract:
activity_type =callcontact_reference =Sarah Johnsonopportunity_reference =Azure Migrationoccurred_at =today
Then resolve the entities.
30. Email Activity
Example:
Record that I emailed Sarah Johnson the revised Azure pricing today.
Extract:
activity_type =emailcontact_reference =Sarah Johnsonsummary =Sent revised Azure pricing.occurred_at =today
No actual email integration is required yet.
We are simply recording the interaction.
31. Meeting Activity
Example:
Record a meeting with Adventure Works yesterday about the Azure migration.
Extract:
activity_type =meetingcompany_reference =Adventure Worksopportunity_reference =Azure Migrationoccurred_at =yesterday
Again, this is manual CRM capture.
Calendar synchronization comes later.
32. Note Activity
Example:
Add a note to Adventure Works: budget approval is expected in September.
Extract:
activity_type =notecompany_reference =Adventure Workssummary =Budget approval is expected in September.
Notes are useful because not every CRM observation is a call, email, or meeting.
33. Notes Still Need Governance
A note may feel harmless, but it becomes authoritative CRM history.
Therefore it should still use:
DraftPrepareConfirmCreateAudit
Consistency is more valuable than special-case shortcuts.
34. Create Activity Draft Tool
Introduce:
create_activity_draft
Conceptually:
class CreateActivityDraftInput(BaseModel): activity_type: ActivityType | None = None subject: str | None = None summary: str | None = None occurred_at_expression: str | None = None company_reference: str | None = None contact_reference: str | None = None opportunity_reference: str | None = None
Notice what is absent:
organization_idcompany_idcontact_idopportunity_idcreated_by_user_idversion
Those are application-owned.
35. Why Accept References Rather Than IDs?
ChatGPT interacts with:
Adventure WorksSarah JohnsonAzure Migration
The application resolves those into:
company_idcontact_idopportunity_id
This keeps entity integrity under server control.
36. Activity Draft Creation Flow
Receive Explicit Facts ↓Validate Activity Type ↓Resolve Time Expression ↓Resolve Company ↓Resolve Contact ↓Resolve Opportunity ↓Cross-Validate Relationships ↓Determine Missing Fields ↓Create ActivityDraft ↓Return Structured Result
Still:
0 Activity rows created
37. Entity Resolution Becomes Central
An Activity may contain three references:
CompanyContactOpportunity
They cannot be resolved independently without checking relationships.
For example:
Sarah Johnson
may belong to:
Adventure Works
and:
Azure Migration
may also belong to:
Adventure Works
Those relationships reinforce each other.
38. Resolve Company First When Explicit
If the user explicitly provides:
Sarah Johnson at Adventure Works
resolve:
Adventure Works
first.
Then search Sarah Johnson inside that Company.
This dramatically reduces ambiguity.
39. Scoped Contact Resolution
Instead of:
Find Sarah Johnson anywhere in tenant
prefer:
Find Sarah JohnsonWHERE company_id = Adventure Works.id
when Company context exists.
This is more precise.
40. Opportunity Resolution
Similarly:
the Azure migration at Adventure Works
should search:
Opportunity name ≈ Azure MigrationANDcompany_id = Adventure Works.id
for the MVP using deterministic normalized matching.
41. Cross-Validation
Suppose the user says:
Record a call with Sarah Johnson at Adventure Works about the Contoso Renewal opportunity.
But:
Sarah Johnson→ Adventure Works
while:
Contoso Renewal→ Contoso
The references conflict.
Quorentra should not silently record the Activity.
42. Relationship Conflict Result
Return:
{ "code": "ENTITY_RELATIONSHIP_CONFLICT", "message": "The selected Contact and Opportunity belong to different Companies."}
ChatGPT can ask the user to clarify.
43. Derive Company from Contact
If the user says:
Record that I called Sarah Johnson.
and Sarah uniquely resolves to:
Sarah JohnsonAdventure Works
Quorentra can derive:
company_id =Adventure Works.id
This is deterministic relationship traversal.
44. Derive Company from Opportunity
Likewise:
Add a note to the Azure Migration opportunity.
If:
Azure Migration→ Adventure Works
then:
company_id
can be derived.
45. Do Not Invent Contact from Company
The reverse is not possible.
If the user says:
I called Adventure Works.
we cannot infer which Contact participated.
The Activity can simply be linked to the Company.
That is valid.
46. Optional Relationships
Examples:
Company only✓Contact + Company✓Opportunity + Company✓Contact + Opportunity + Company✓Contact only✓ if Company can be derivedOpportunity only✓ if Company can be derived
But:
No relationships✗
for the MVP.
47. Missing Fields
Suppose:
Record an activity for Adventure Works.
We know the Company.
But we do not know:
activity_typesummaryoccurred_at
Return:
{ "status": "incomplete", "missing_required_fields": [ "activity_type", "summary", "occurred_at" ]}
48. Conversational Completion
ChatGPT can ask:
What happened?
User:
I called Sarah about the Azure migration.
Now we have:
activity_type =callsummary =Discussed the Azure migration.
If the date is still missing:
When did the call happen?
This is multi-turn draft completion.
49. Do Not Ask Unnecessary Questions
If the user says:
Record that I called Sarah Johnson at Adventure Works today about the Azure migration and she requested revised pricing.
everything required is available.
Proceed directly to preparation.
The system should not ask:
What type of activity?Which company?When did it happen?
when those facts are already explicit.
50. Update Activity Draft
Introduce:
update_activity_draft
Conceptually:
class UpdateActivityDraftInput(BaseModel): draft_id: UUID activity_type: ActivityType | None = None subject: str | None = None summary: str | None = None occurred_at_expression: str | None = None company_reference: str | None = None contact_reference: str | None = None opportunity_reference: str | None = None
Every update reruns relevant validation and resolution.
51. Summary Should Preserve Meaning
The user says:
She asked for revised pricing.
A good stored summary might be:
Customer requested revised pricing.
That is a faithful normalization.
But avoid turning it into:
Customer expressed strong purchasing intent and requesteda 15% discount due to budget constraints.
Those facts were never supplied.
52. AI Summarization Must Be Conservative
Even simple CRM summaries can introduce hallucination.
The rule should be:
Preserve user-provided meaning; improve structure, not substance.
This becomes increasingly important as Activity data later feeds AI recommendations.
53. Why Grounded Activity Data Matters
Suppose an AI later asks:
Is the Azure Migration opportunity at risk?
If the Activity timeline contains invented details, the risk analysis becomes unreliable.
Therefore:
Good AI CRM=Good underlying CRM evidence
Part 22 is part of that evidence architecture.
54. Prepare Activity Creation
Once the ActivityDraft is complete:
prepare_activity_creation
Input:
{ "draft_id": "..."}
55. Preparation Flow
Load ActivityDraft ↓Verify Tenant ↓Verify User ↓Verify activities.create ↓Verify Not Expired ↓Resolve/Revalidate Entities ↓Validate Relationships ↓Validate Timestamp ↓Validate Summary ↓Create MutationRequest ↓Mark Draft Prepared ↓Return Confirmation
No Activity exists yet.
56. Mutation Type
Use:
mutation_type =activity.create
Proposed values might be:
{ "activity_type": "call", "occurred_at": "2026-08-02T07:30:00Z", "company_id": "...", "contact_id": "...", "opportunity_id": "...", "summary": "Discussed the Azure migration. Customer requested revised pricing."}
57. Confirmation Widget
Create:
chatgpt-ui/src/mutations/└── ActivityCreationConfirmation.tsx
Example:
┌──────────────────────────────────────────────┐│ QUORENTRA ││ Record Activity ││ ││ Call ││ ││ Sarah Johnson ││ Adventure Works ││ ││ Opportunity ││ Azure Migration ││ ││ Date ││ 2 August 2026 ││ ││ Summary ││ Discussed the Azure migration. ││ Customer requested revised pricing. ││ ││ [Cancel] [Record Activity] │└──────────────────────────────────────────────┘
58. Confirmation Is Especially Important for History
Activities become historical evidence.
Once recorded, later AI may use them to answer:
What did the customer say?
Therefore users should see what will be stored.
The confirmation UI acts as an evidence-quality checkpoint.
59. Show Resolved Entities
The widget should display:
ContactCompanyOpportunity
when present.
This allows the user to catch entity-resolution mistakes before creation.
60. Show the Resolved Date
If the user said:
yesterday
the confirmation should show:
1 August 2026
not:
Yesterday
The user should confirm the actual stored meaning.
61. Editing Before Creation
User:
Actually, the call was yesterday, not today.
Update:
ActivityDraft.occurred_at
Any existing prepared MutationRequest becomes stale.
Prepare again.
Show the new confirmation.
62. Execute Activity Creation
Introduce:
create_activity
Input:
{ "mutation_request_id": "..."}
As before, execution accepts no arbitrary Activity fields.
63. Execution Flow
Load MutationRequest ↓Verify Tenant ↓Verify User ↓Verify activities.create ↓Verify Pending ↓Verify Not Expired ↓Load ActivityDraft ↓Verify Draft Matches ↓Revalidate Entities ↓Revalidate Relationships ↓Revalidate Timestamp ↓Create Activity ↓Create AuditEvent ↓Mark Mutation Executed ↓Mark ActivityDraft Converted ↓Commit
64. Transaction Boundary
Use:
BEGINcreate Activitycreate AuditEventmark MutationRequest executedmark ActivityDraft convertedCOMMIT
On failure:
ROLLBACK
The timeline should never contain a partially committed mutation.
65. Initial Version
The Activity starts with:
version = 1
This keeps the domain compatible with future updates.
66. Activity Audit Event
Use:
action =activity.created
Store:
organization_idactor_user_identity_identity_typenew_valuesinvocation_sourcemutation_request_idworkflow_idcreated_at
67. Avoid Duplicating Sensitive Content Everywhere
Activity summaries may contain commercially sensitive information.
For example:
Customer budgetPricing objectionsContract concernsInternal stakeholdersSecurity issues
Do not copy full Activity summaries into ordinary operational logs.
Keep authoritative Activity content in the CRM database.
Keep audit and logging proportional.
68. Authoritative Result
After successful commit:
{ "success": true, "activity": { "id": "...", "activity_type": "call", "occurred_at": "2026-08-02T07:30:00Z", "company": { "id": "...", "name": "Adventure Works" }, "contact": { "id": "...", "name": "Sarah Johnson" }, "opportunity": { "id": "...", "name": "Azure Migration" }, "summary": "Discussed the Azure migration. Customer requested revised pricing.", "version": 1 }}
Only now is the Activity part of CRM history.
69. Success Widget
Render:
┌──────────────────────────────────────────────┐│ QUORENTRA ││ Activity Recorded ││ ││ Call with Sarah Johnson ││ Adventure Works ││ ││ Azure Migration ││ 2 August 2026 ││ ││ Customer requested revised pricing. ││ ││ [View Timeline] │└──────────────────────────────────────────────┘
70. The Company Timeline
Now introduce:
get_company_timeline
The user can ask:
Show me the recent activity for Adventure Works.
Quorentra resolves the Company and returns Activities ordered by:
occurred_at DESC
71. Example Company Timeline
Adventure Works────────────────────────────────────02 Aug 2026☎ Call — Sarah JohnsonAzure MigrationCustomer requested revised pricing.28 Jul 2026◉ MeetingAzure MigrationArchitecture workshop completed.23 Jul 2026✉ Email — Sarah JohnsonSent technical proposal.20 Jul 2026☎ Call — Sarah JohnsonDiscussed migration requirements.
This is the beginning of useful CRM history.
72. Contact Timeline
Introduce:
get_contact_timeline
User:
What happened recently with Sarah Johnson?
Result:
Sarah JohnsonAdventure Works02 Aug — CallCustomer requested revised pricing.23 Jul — EmailSent technical proposal.20 Jul — CallDiscussed migration requirements.
73. Opportunity Timeline
Introduce:
get_opportunity_timeline
User:
Show me the history of the Azure Migration opportunity.
Result:
Azure MigrationAdventure Works€90,00002 Aug — CallCustomer requested revised pricing.28 Jul — MeetingArchitecture workshop completed.23 Jul — EmailSent technical proposal.
This gives Opportunity context far beyond stage and amount.
74. One Activity Can Appear in Multiple Timelines
This is important.
The same Activity:
Call with Sarah Johnsonabout Azure Migrationat Adventure Works
can appear in:
Company TimelineContact TimelineOpportunity Timeline
We should not create three copies.
Create:
1 Activity
with relationships to:
CompanyContactOpportunity
Then query it from different perspectives.
75. Avoid Timeline Duplication
Do not create:
CompanyActivityContactActivityOpportunityActivity
as three independent records for the same event.
Use one authoritative Activity entity.
This prevents divergence.
76. Activity Indexes
Useful database indexes include:
organization_id, occurred_atorganization_id, company_id, occurred_atorganization_id, contact_id, occurred_atorganization_id, opportunity_id, occurred_at
These support fast timeline queries.
77. Tenant Scope on Every Timeline
Every timeline query must include:
organization_id
even when querying by entity ID.
Never assume an entity ID alone provides sufficient tenant isolation.
78. Pagination
Timelines can grow large.
Support:
limitcursor
or:
limitoffset
for the MVP.
Cursor pagination is preferable long-term.
79. Default Timeline Limit
A reasonable first default:
20 activities
Do not return thousands of historical events to ChatGPT unnecessarily.
80. Timeline Ordering
Default:
occurred_at DESC
not:
created_at DESC
Why?
A user may record yesterday’s call today.
The business timeline should reflect when the event happened, not when it was entered.
81. Created Time Still Matters
Keep:
created_at
for audit and operational purposes.
This lets us distinguish:
Occurred:1 AugustRecorded:2 August
Both are meaningful.
82. Backdated Activities
Backdating should be allowed.
Example:
Record that we met Adventure Works last Thursday.
That is normal CRM behavior.
The Activity occurred in the past.
83. Future-Dated Activities?
A future event is usually not an Activity.
It is more likely a:
TaskMeetingCalendar Event
Therefore if:
occurred_at > now
Quorentra should normally reject the Activity or ask whether the user intended to create a future Task or Meeting.
This is an important domain distinction.
84. Example Future-Date Handling
User:
Record a call with Sarah tomorrow.
Quorentra should not create a historical Call Activity.
ChatGPT can say:
That sounds like a future follow-up rather than a completed activity. Would you like to create a task instead?
We will build Tasks next.
85. Activity Versus Task
This distinction is fundamental:
Activity=Something that happenedTask=Something that should happen
Examples:
Called Sarah today→ ActivityCall Sarah tomorrow→ Task
This simple rule will shape Part 23.
86. Meeting Ambiguity
Likewise:
Meeting with Sarah yesterday.
means:
Activity
while:
Meeting with Sarah next Tuesday.
means:
Future Meeting / Task
Do not treat both as the same domain entity.
87. Activity Search
Introduce:
search_activities
with filters such as:
activity_typecompany_referencecontact_referenceopportunity_referencefrom_dateto_datelimit
This enables structured retrieval.
88. Example Query
User:
Show me all calls with Adventure Works this month.
ChatGPT can call:
search_activities
with:
activity_type = callcompany = Adventure Worksfrom_date = 1 August 2026to_date = 31 August 2026
The server performs the actual query.
89. Do Not Make ChatGPT Filter Huge Results
Avoid:
Return all activities→ let model find August calls
Instead:
Structured filter→ database query→ bounded result
This improves:
AccuracyPrivacyLatencyToken usage
90. Activity Detail
Introduce:
get_activity
so ChatGPT can retrieve one authoritative Activity.
For example:
Show me the call where Sarah asked for revised pricing.
Search can identify candidates.
Then detail retrieval returns the selected record.
91. Timeline Widget
Create:
chatgpt-ui/src/activities/├── ActivityTimeline.tsx├── ActivityCard.tsx└── ActivityDetail.tsx
A timeline widget can visually distinguish:
CallEmailMeetingNote
while keeping a consistent layout.
92. Activity Card
Example:
┌───────────────────────────────────────────┐│ CALL ││ 2 Aug 2026 ││ ││ Sarah Johnson · Adventure Works ││ Azure Migration ││ ││ Customer requested revised pricing. │└───────────────────────────────────────────┘
93. Email Card
┌───────────────────────────────────────────┐│ EMAIL ││ 23 Jul 2026 ││ ││ Sarah Johnson · Adventure Works ││ Azure Migration ││ ││ Sent technical proposal. │└───────────────────────────────────────────┘
94. Meeting Card
┌───────────────────────────────────────────┐│ MEETING ││ 28 Jul 2026 ││ ││ Adventure Works ││ Azure Migration ││ ││ Architecture workshop completed. │└───────────────────────────────────────────┘
95. Note Card
┌───────────────────────────────────────────┐│ NOTE ││ 30 Jul 2026 ││ ││ Adventure Works ││ ││ Budget approval expected in September. │└───────────────────────────────────────────┘
96. Keep Widgets Data-Driven
Do not make separate backend endpoints solely because UI cards differ.
All four are:
Activity
with:
activity_type
The frontend determines presentation.
This keeps the domain clean.
97. Activity Draft Tests
Test:
create draftupdate draftcancel draftexpire draftconvert drafttenant isolationuser ownership
98. Activity Type Tests
Verify:
call ✓email ✓meeting ✓note ✓unknown ✗
Invalid Activity types must fail structurally.
99. Required-Field Tests
Verify:
activity_type requiredsummary requiredoccurred_at requiredat least one CRM relationship required
100. Relative Date Tests
Test:
todayyesterdaylast Friday2 August 20262 August at 14:00
using a fixed timezone and clock in tests.
Do not let tests depend on the actual current date.
101. Timezone Tests
Verify the same local input produces the correct UTC timestamp for:
Europe/AmsterdamAmerica/New_YorkAsia/Tokyo
according to timezone rules.
102. Company Resolution Tests
Test:
explicit Companyderived Company from Contactderived Company from Opportunityambiguous Companymissing Companycross-tenant Company
103. Contact Resolution Tests
Test:
unique ContactCompany-scoped Contactambiguous Contactmissing Contactcross-tenant Contact
104. Opportunity Resolution Tests
Test:
unique OpportunityCompany-scoped Opportunityambiguous Opportunitymissing Opportunitycross-tenant Opportunity
105. Relationship Conflict Tests
Create:
Sarah Johnson→ Adventure WorksContoso Renewal→ Contoso
Then attempt an Activity referencing both.
Expected:
ENTITY_RELATIONSHIP_CONFLICT
No Activity created.
106. Derived Company Test
User references only:
Sarah Johnson
Sarah belongs to Adventure Works.
Expected:
contact_id =Sarah.idcompany_id =AdventureWorks.id
107. Permission Tests
Viewer:
create_activity_draft → deniedprepare_activity_creation → deniedcreate_activity → denied
Member:
allowed
according to the MVP matrix.
108. Permission Change Test
Prepare an Activity.
Remove:
activities.create
before execution.
Expected:
create_activity → denied
No Activity exists.
109. Preparation Tests
Verify:
incomplete draft cannot prepareexpired draft cannot preparefuture Activity rejectedrelationship conflict rejectedcomplete Activity can preparepreparation creates no Activity
110. Edit-After-Prepare Test
Prepare:
occurred_at =2 August
Then user says:
Actually it happened yesterday.
Update the draft.
Expected:
old MutationRequest invalidatednew confirmation required
111. Execution Tests
Successful execution must:
create exactly one Activityuse active tenantuse authenticated actoruse resolved entity IDsuse confirmed timestampuse confirmed summaryset version = 1create AuditEventexecute MutationRequestconvert ActivityDraftreturn authoritative Activity
112. Idempotency Test
Execute the same Activity MutationRequest twice.
Expected:
1 Activity1 AuditEvent1 logical result
Never:
2 timeline entries
113. Transaction Failure Test
Simulate AuditEvent failure.
Expected:
Activity rolled backMutationRequest not executedActivityDraft not converted
114. Timeline Tests
Create Activities with different:
occurred_atcreated_at
Verify timeline order uses:
occurred_at DESC
115. Company Timeline Isolation Test
Tenant A and Tenant B both have:
Adventure Works
Ensure Tenant A’s timeline never includes Tenant B’s Activities.
116. Contact Timeline Test
An Activity linked to Sarah Johnson should appear in:
Sarah's Contact timeline
when queried.
117. Opportunity Timeline Test
An Activity linked to Azure Migration should appear in:
Azure Migration timeline
118. Shared Activity Test
One Activity linked to:
Adventure WorksSarah JohnsonAzure Migration
should appear in all three timelines.
Verify there is still only:
1 Activity row
119. One-Shot Workflow Test
User:
Record that I called Sarah Johnson at Adventure Works today about the Azure migration. She asked for revised pricing.
Expected:
Activity Type resolvedContact resolvedCompany resolvedOpportunity resolvedDate resolvedSummary extractedDraft completePreparation available
No unnecessary questions.
120. Multi-Turn Workflow Test
User:
Record a call with Sarah Johnson.
ChatGPT may need:
DateSummary
User:
Yesterday. We discussed revised Azure pricing.
Draft becomes complete.
Then prepare.
121. Correction Test
User:
Actually, it was a meeting, not a call.
Update:
activity_type =meeting
Reprepare.
Require fresh confirmation.
122. Cancellation Test
User cancels Activity confirmation.
Expected:
No ActivityNo Activity AuditEventDraft cancelled or retained according to policy
123. Logging
Useful structured operational fields:
activity_draft_idactivity_idorganization_iduser_idworkflow_idactivity_typeresolution_resultmutation_request_idresultduration_ms
Avoid logging:
full summaryemail contentmeeting notes
unless explicitly required.
124. Observability
Useful metrics might include:
activity_drafts_created_totalactivities_created_totalactivity_creation_failures_totalactivity_resolution_conflicts_totalactivity_confirmation_cancellations_totalactivity_creation_duration_seconds
Later these can support operational dashboards.
125. Version Update
Part 22 introduces:
Activity DomainActivity DraftsCall CaptureEmail CaptureMeeting CaptureNote CaptureMulti-Entity ResolutionRelationship ValidationCustomer Timelines
Update:
app/core/constants.py
from:
APP_VERSION = "0.8.0"
to:
APP_VERSION = "0.9.0"
126. Quorentra 0.9.0
Our modular MVP now looks like:
Platform├── FastAPI ✓├── PostgreSQL ✓├── SQLAlchemy ✓└── Alembic ✓Identity├── Organizations ✓├── Users ✓├── Memberships ✓├── Authentication ✓└── JWT ✓Security├── TenantContext ✓├── Tenant Isolation ✓├── RBAC ✓├── Read Permissions ✓├── Create Permissions ✓└── Update Permissions ✓CRM├── Companies ✓├── Contacts ✓├── Opportunities ✓└── Activities ✓Activity Types├── Calls ✓├── Emails ✓├── Meetings ✓└── Notes ✓Sales├── Pipeline ✓├── Opportunity Stages ✓├── Probability ✓└── Weighted Pipeline ✓Interfaces├── REST ✓├── MCP ✓└── ChatGPT ✓ChatGPT UI├── Pipeline ✓├── Company Detail ✓├── Contact Detail ✓├── Opportunity Detail ✓├── Activity Timeline ✓├── Activity Detail ✓├── Mutation Confirmation ✓├── Company Creation ✓├── Contact Creation ✓├── Opportunity Creation ✓└── Activity Creation ✓Mutation Governance├── Mutation Requests ✓├── Confirmation ✓├── Expiry ✓├── Cancellation ✓├── Idempotency ✓├── Transactions ✓├── Optimistic Concurrency ✓└── Audit Events ✓Conversational Creation├── Company Drafts ✓├── Contact Drafts ✓├── Opportunity Drafts ✓├── Activity Drafts ✓├── Entity Resolution ✓├── Relative Time Resolution ✓├── Relationship Validation ✓├── Missing-Field Detection ✓├── Field Provenance ✓├── Multi-Turn Completion ✓├── Draft Correction ✓├── Prepared Creation ✓└── Safe Execution ✓CRM Memory├── Company Timeline ✓├── Contact Timeline ✓├── Opportunity Timeline ✓├── Calls ✓├── Emails ✓├── Meetings ✓└── Notes ✓Workflow Composition├── Company → Contact ✓├── Company → Opportunity ✓├── Contact → Activity ✓├── Company → Activity ✓├── Opportunity → Activity ✓├── Workflow Correlation ✓└── Resume Original Intent ✓ChatGPT CRM Operations├── Read Pipeline ✓├── Read Companies ✓├── Read Contacts ✓├── Read Opportunities ✓├── Read Activities ✓├── Create Company ✓├── Create Contact ✓├── Create Opportunity ✓├── Update Opportunity Stage ✓├── Record Activity ✓├── Create Task -└── Complete Task -Tasks -AI Intelligence -
127. Acceptance Criteria
Part 22 is complete when:
✓ Part 21 regression suite remains green✓ activities.read permission exists✓ activities.create permission exists✓ Viewer cannot create Activities✓ authorized roles can create Activities✓ execution rechecks permission✓ Activity model exists✓ Activity migration applies✓ Activity is tenant-scoped✓ Activity records authenticated creator✓ Activity version starts at 1✓ CALL Activity type exists✓ EMAIL Activity type exists✓ MEETING Activity type exists✓ NOTE Activity type exists✓ unsupported Activity types are rejected✓ ActivityDraft exists✓ ActivityDraft migration applies✓ ActivityDraft is tenant-scoped✓ ActivityDraft is user-scoped✓ ActivityDraft supports expiry✓ ActivityDraft supports cancellation✓ ActivityDraft supports conversion✓ ActivityDraft supports workflow_id✓ activity_type is required✓ summary is required✓ occurred_at is required✓ at least one CRM relationship is required✓ subject can remain optional✓ organization_id comes from TenantContext✓ created_by_user_id comes from authentication✓ ChatGPT cannot supply authoritative tenant✓ ChatGPT cannot impersonate Activity creator✓ create_activity_draft exists✓ update_activity_draft exists✓ draft tools create no Activity✓ relative date resolution works✓ today resolves deterministically✓ yesterday resolves deterministically✓ explicit dates resolve correctly✓ timezone handling exists✓ authoritative timestamps are stored✓ relative expressions are not stored as authoritative dates✓ Company resolution works✓ Contact resolution works✓ Opportunity resolution works✓ Contact resolution can be Company-scoped✓ Opportunity resolution can be Company-scoped✓ Company can be derived from Contact✓ Company can be derived from Opportunity✓ Contact is not invented from Company✓ entity relationships are cross-validated✓ conflicting Company/Contact/Opportunity relationships are rejected✓ cross-tenant entities never resolve✓ missing-field detection is server-owned✓ incomplete drafts remain drafts✓ complete requests require no unnecessary questions✓ multi-turn completion works✓ user-provided meaning is preserved✓ summaries do not invent unsupported facts✓ field provenance records resolved/derived values✓ prepare_activity_creation exists✓ incomplete Activity cannot prepare✓ expired Activity cannot prepare✓ relationship conflicts cannot prepare✓ future historical Activities are rejected or redirected✓ preparation creates no Activity✓ activity.create MutationRequest exists✓ proposed values match ActivityDraft✓ preparation expiration works✓ editing ActivityDraft invalidates old preparation✓ Activity confirmation widget exists✓ Activity type is visible✓ resolved Company is visible✓ resolved Contact is visible when present✓ resolved Opportunity is visible when present✓ resolved absolute date is visible✓ summary is visible✓ action says Record Activity✓ Cancel creates no Activity✓ create_activity accepts mutation_request_id only✓ arbitrary execution-time Activity values are rejected✓ execution rechecks tenant✓ execution rechecks user✓ execution rechecks permission✓ execution rechecks request state✓ execution rechecks expiry✓ execution revalidates entities✓ execution revalidates relationships✓ execution revalidates timestamp✓ Activity creation is transactional✓ Activity ID is server-generated✓ Activity version starts at 1✓ Activity AuditEvent is transactional✓ MutationRequest becomes executed✓ ActivityDraft becomes converted✓ failed transaction rolls back✓ duplicate execution is idempotent✓ one MutationRequest creates at most one Activity✓ one logical AuditEvent is created✓ authoritative Activity result is returned✓ ChatGPT reports success only after authoritative execution✓ Activity success widget uses authoritative data✓ get_company_timeline exists✓ get_contact_timeline exists✓ get_opportunity_timeline exists✓ search_activities exists✓ get_activity exists✓ timeline queries are tenant-scoped✓ timeline queries are bounded✓ timeline pagination exists✓ timeline sorts by occurred_at✓ backdated Activities appear in correct historical order✓ one Activity can appear in Company timeline✓ same Activity can appear in Contact timeline✓ same Activity can appear in Opportunity timeline✓ shared Activity is not duplicated in persistence✓ future event is distinguished from historical Activity✓ "called today" maps to Activity✓ "call tomorrow" does not silently map to Activity✓ Activity operational logging minimizes CRM content✓ basic Activity metrics exist✓ Quorentra reports version 0.9.0
Most importantly:
A user can now describe a completed customer interaction naturally, allow Quorentra to resolve the relevant Company, Contact, Opportunity, and time context, review the structured historical record, explicitly confirm it, and add it safely to the customer’s permanent CRM timeline.
128. What We Have Achieved
Quorentra can now understand:
Record that I called Sarah Johnson at Adventure Works today about the Azure migration. She asked for revised pricing.
and transform it into:
Natural Language ↓Activity Intent ↓Call ↓Sarah Johnson ↓Adventure Works ↓Azure Migration ↓2 August 2026 ↓Customer requested revised pricing ↓Activity Draft ↓Entity Validation ↓Confirmation ↓Activity Creation ↓Audit ↓Customer Timeline
This is a significant change.
Quorentra no longer stores only commercial objects.
It begins storing commercial history.
129. CRM State Versus CRM History
We can now make an important distinction.
Our existing entities describe state:
CompanyContactOpportunity
Activities describe history:
What happenedWhen it happenedWho was involvedWhich opportunity it concerned
Together:
CRM State+CRM History=CRM Context
That context is the foundation for future AI capabilities.
130. The Customer Timeline Becomes Strategic
Consider Adventure Works after several weeks:
Adventure Works│├── Contacts│ ├── Sarah Johnson — IT Director│ └── Lisa Chen — Procurement Manager│├── Opportunities│ └── Azure Migration — €90,000│└── Timeline ├── 02 Aug — Call │ Sarah requested revised pricing. │ ├── 28 Jul — Meeting │ Architecture workshop completed. │ ├── 23 Jul — Email │ Technical proposal sent. │ └── 20 Jul — Call Migration requirements discussed.
This is beginning to look like a genuinely useful CRM.
131. ChatGPT Can Now Ask Better Questions
Once this timeline exists, users can ask:
What is happening with Adventure Works?
Quorentra can retrieve:
CompanyContactsOpportunityRecent Activities
and provide a grounded answer.
The important word is:
grounded
ChatGPT does not need to invent the relationship history.
Quorentra supplies it.
132. But We Still Have a Major Gap
The Activity timeline records:
what happened
But it does not represent:
what needs to happen next
After:
Sarah requested revised pricing.
the natural next action might be:
Prepare revised pricingSend updated proposalCall SarahSchedule review meeting
These are not Activities yet.
They are future obligations.
That means we need:
Task
133. Activity and Task Form a Natural Pair
The distinction is simple:
PAST / COMPLETED ↓ ActivityFUTURE / REQUIRED ↓ Task
Examples:
Called Sarah today→ ActivityCall Sarah Friday→ TaskSent proposal yesterday→ ActivitySend revised proposal tomorrow→ TaskMet customer Monday→ ActivitySchedule technical workshop→ Task
This gives Quorentra a clean temporal model.
134. Tasks Introduce New Concepts
Task management will require:
due_atstatuspriorityassigneecompletion
and relationships to:
CompanyContactOpportunity
It will also introduce our first important transition from:
Open
to:
Completed
135. Tasks Can Generate Activities
There is another interesting relationship.
Suppose:
Task:Call Sarah Friday
The user later says:
Mark the Sarah call complete. We discussed revised pricing.
Quorentra could eventually:
Complete Task +Create Call Activity
This gives us another multi-entity workflow.
136. Tasks Also Make ChatGPT Proactive
Once Tasks exist, users can ask:
What do I need to do today?
What is overdue?
What should I follow up on?
Show my tasks for Adventure Works.
What follow-ups are due this week?
That changes ChatGPT from a CRM data-entry interface into a daily work interface.
137. Keep Part 23 Modular
We still should not build:
Complex workflow automationRecurring tasksAI task generationEmail remindersPush notificationsCalendar synchronizationEscalation rules
yet.
Start with:
Create TaskRead TasksComplete Task
That is enough for a valuable next increment.
138. Next Article
Creating Tasks from ChatGPT — Follow-Ups, Due Dates, Priorities, Assignments, and Completion
We will introduce:
tasks.readtasks.createtasks.completeTask modelTaskDraftTask Status├── Open└── CompletedTask Priority├── Low├── Normal├── High└── UrgentTitleDescriptionDue dateAssigneeCompany relationshipContact relationshipOpportunity relationshipRelative date resolutionEntity resolutionAssignment resolutionMissing-field detectionTask validationTask confirmationPrepared Task mutationsIdempotent Task creationTask AuditEventsTask completionCompletion confirmationOpen task listsOverdue task queriesToday's tasksCompany tasksContact tasksOpportunity tasksTask widgets
Our target interaction will be:
User:"Remind me to call Sarah Johnson atAdventure Works Friday about the revisedAzure migration pricing."
Quorentra will structure:
TaskTitle:Call Sarah JohnsonDue:FridayContact:Sarah JohnsonCompany:Adventure WorksOpportunity:Azure MigrationDescription:Discuss revised pricing.Status:OpenPriority:Normal
After confirmation:
Create Task ↓Open Task List
Later:
User:"I called Sarah. Mark the task complete.She asked us to send the revised proposalnext Tuesday."
Quorentra can begin orchestrating:
Complete Existing Task ↓Record Call Activity ↓Create New Follow-Up Task
That is where the modular architecture starts to become particularly powerful.
We are building small, governed CRM primitives:
CompanyContactOpportunityActivityTask
while allowing ChatGPT to compose those primitives into increasingly natural business workflows.
That is exactly the architecture we want for Quorentra: A Modular, ChatGPT-Native AI CRM.