Quorentra

Quorentra Recording CRM Activities from ChatGPT: Building from Zero — Part 22

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.

Quorentra Recording CRM Activities from ChatGPT: Building from Zero — Part 22
Quorentra Recording CRM Activities from ChatGPT: Building from Zero — Part 22

1. Introduction

Quorentra now understands the three core commercial entities of a CRM:

Company
Contact
Opportunity

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 Works
Contact:
Sarah Johnson
Opportunity:
Azure Migration
Value:
€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:
Call
Contact:
Sarah Johnson
Company:
Adventure Works
Opportunity:
Azure Migration
Date:
Today
Summary:
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:

Call
Email
Meeting
Note
Proposal
Follow-up
Decision
Status 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 synchronization
Gmail synchronization
Teams meetings
Google Calendar
Zoom
Meeting transcription
Call recording
Automatic email capture
Slack
WhatsApp
Telephony

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:

Call
Email
Meeting
Note

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-ui
npm 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.update
Contacts
├── contacts.read
├── contacts.create
└── contacts.update
Opportunities
├── opportunities.read
├── opportunities.create
└── opportunities.update
Activities
├── activities.read
└── activities.create

Later we can add:

activities.update
activities.delete

For now, read and create are sufficient.


8. Example Role Matrix

For the MVP:

RoleRead ActivitiesCreate 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_COMPLETED
PROPOSAL_SENT
DOCUMENT_SHARED
DEMO
WORKSHOP
STATUS_CHANGE
SYSTEM_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:

id
organization_id
activity_type
subject
summary
occurred_at
company_id
contact_id
opportunity_id
created_by_user_id
version
created_at
updated_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_id
opportunity_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_type
occurred_at
summary

and at least one relationship:

company_id
OR
contact_id
OR
opportunity_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:

subject
summary

For example:

Subject:
Azure migration pricing discussion
Summary:
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:

Call
Sarah Johnson
Azure 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.

FieldSource
idSystem
organization_idTenantContext
activity_typeUser intent
subjectUser or server-derived
summaryUser
occurred_atUser context / server-resolved
company_idEntity resolution
contact_idEntity resolution
opportunity_idEntity resolution
created_by_user_idAuthenticated user
versionSystem
created_atSystem
updated_atSystem

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:
Call
Contact:
Sarah
Date:
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:

incomplete
complete
prepared
cancelled
expired
converted

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:

today
yesterday
this morning
Friday
last Tuesday
2 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/Amsterdam
Input:
today at 14:00
Stored:
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 =
call
contact_reference =
Sarah Johnson
opportunity_reference =
Azure Migration
occurred_at =
today

Then resolve the entities.


30. Email Activity

Example:

Record that I emailed Sarah Johnson the revised Azure pricing today.

Extract:

activity_type =
email
contact_reference =
Sarah Johnson
summary =
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 =
meeting
company_reference =
Adventure Works
opportunity_reference =
Azure Migration
occurred_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 =
note
company_reference =
Adventure Works
summary =
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:

Draft
Prepare
Confirm
Create
Audit

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_id
company_id
contact_id
opportunity_id
created_by_user_id
version

Those are application-owned.


35. Why Accept References Rather Than IDs?

ChatGPT interacts with:

Adventure Works
Sarah Johnson
Azure Migration

The application resolves those into:

company_id
contact_id
opportunity_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:

Company
Contact
Opportunity

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 Johnson
WHERE 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 Migration
AND
company_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 Johnson
Adventure 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 derived
Opportunity 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_type
summary
occurred_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 =
call
summary =
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 requested
a 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:

Contact
Company
Opportunity

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:

BEGIN
create Activity
create AuditEvent
mark MutationRequest executed
mark ActivityDraft converted
COMMIT

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_id
actor_user_id
entity_id
entity_type
new_values
invocation_source
mutation_request_id
workflow_id
created_at

67. Avoid Duplicating Sensitive Content Everywhere

Activity summaries may contain commercially sensitive information.

For example:

Customer budget
Pricing objections
Contract concerns
Internal stakeholders
Security 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 Johnson
Azure Migration
Customer requested revised pricing.
28 Jul 2026
◉ Meeting
Azure Migration
Architecture workshop completed.
23 Jul 2026
✉ Email — Sarah Johnson
Sent technical proposal.
20 Jul 2026
☎ Call — Sarah Johnson
Discussed 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 Johnson
Adventure Works
02 Aug — Call
Customer requested revised pricing.
23 Jul — Email
Sent technical proposal.
20 Jul — Call
Discussed migration requirements.

73. Opportunity Timeline

Introduce:

get_opportunity_timeline

User:

Show me the history of the Azure Migration opportunity.

Result:

Azure Migration
Adventure Works
€90,000
02 Aug — Call
Customer requested revised pricing.
28 Jul — Meeting
Architecture workshop completed.
23 Jul — Email
Sent 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 Johnson
about Azure Migration
at Adventure Works

can appear in:

Company Timeline
Contact Timeline
Opportunity Timeline

We should not create three copies.

Create:

1 Activity

with relationships to:

Company
Contact
Opportunity

Then query it from different perspectives.


75. Avoid Timeline Duplication

Do not create:

CompanyActivity
ContactActivity
OpportunityActivity

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_at
organization_id, company_id, occurred_at
organization_id, contact_id, occurred_at
organization_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:

limit
cursor

or:

limit
offset

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 August
Recorded:
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:

Task
Meeting
Calendar 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 happened
Task
=
Something that should happen

Examples:

Called Sarah today
→ Activity
Call 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_type
company_reference
contact_reference
opportunity_reference
from_date
to_date
limit

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 = call
company = Adventure Works
from_date = 1 August 2026
to_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:

Accuracy
Privacy
Latency
Token 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:

Call
Email
Meeting
Note

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 draft
update draft
cancel draft
expire draft
convert draft
tenant isolation
user ownership

98. Activity Type Tests

Verify:

call ✓
email ✓
meeting ✓
note ✓
unknown ✗

Invalid Activity types must fail structurally.


99. Required-Field Tests

Verify:

activity_type required
summary required
occurred_at required
at least one CRM relationship required

100. Relative Date Tests

Test:

today
yesterday
last Friday
2 August 2026
2 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/Amsterdam
America/New_York
Asia/Tokyo

according to timezone rules.


102. Company Resolution Tests

Test:

explicit Company
derived Company from Contact
derived Company from Opportunity
ambiguous Company
missing Company
cross-tenant Company

103. Contact Resolution Tests

Test:

unique Contact
Company-scoped Contact
ambiguous Contact
missing Contact
cross-tenant Contact

104. Opportunity Resolution Tests

Test:

unique Opportunity
Company-scoped Opportunity
ambiguous Opportunity
missing Opportunity
cross-tenant Opportunity

105. Relationship Conflict Tests

Create:

Sarah Johnson
→ Adventure Works
Contoso 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.id
company_id =
AdventureWorks.id

107. Permission Tests

Viewer:

create_activity_draft → denied
prepare_activity_creation → denied
create_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 prepare
expired draft cannot prepare
future Activity rejected
relationship conflict rejected
complete Activity can prepare
preparation 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 invalidated
new confirmation required

111. Execution Tests

Successful execution must:

create exactly one Activity
use active tenant
use authenticated actor
use resolved entity IDs
use confirmed timestamp
use confirmed summary
set version = 1
create AuditEvent
execute MutationRequest
convert ActivityDraft
return authoritative Activity

112. Idempotency Test

Execute the same Activity MutationRequest twice.

Expected:

1 Activity
1 AuditEvent
1 logical result

Never:

2 timeline entries

113. Transaction Failure Test

Simulate AuditEvent failure.

Expected:

Activity rolled back
MutationRequest not executed
ActivityDraft not converted

114. Timeline Tests

Create Activities with different:

occurred_at
created_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 Works
Sarah Johnson
Azure 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 resolved
Contact resolved
Company resolved
Opportunity resolved
Date resolved
Summary extracted
Draft complete
Preparation available

No unnecessary questions.


120. Multi-Turn Workflow Test

User:

Record a call with Sarah Johnson.

ChatGPT may need:

Date
Summary

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 Activity
No Activity AuditEvent
Draft cancelled or retained according to policy

123. Logging

Useful structured operational fields:

activity_draft_id
activity_id
organization_id
user_id
workflow_id
activity_type
resolution_result
mutation_request_id
result
duration_ms

Avoid logging:

full summary
email content
meeting notes

unless explicitly required.


124. Observability

Useful metrics might include:

activity_drafts_created_total
activities_created_total
activity_creation_failures_total
activity_resolution_conflicts_total
activity_confirmation_cancellations_total
activity_creation_duration_seconds

Later these can support operational dashboards.


125. Version Update

Part 22 introduces:

Activity Domain
Activity Drafts
Call Capture
Email Capture
Meeting Capture
Note Capture
Multi-Entity Resolution
Relationship Validation
Customer 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:

Company
Contact
Opportunity

Activities describe history:

What happened
When it happened
Who was involved
Which 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:

Company
Contacts
Opportunity
Recent 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 pricing
Send updated proposal
Call Sarah
Schedule 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
Activity
FUTURE / REQUIRED
Task

Examples:

Called Sarah today
→ Activity
Call Sarah Friday
→ Task
Sent proposal yesterday
→ Activity
Send revised proposal tomorrow
→ Task
Met customer Monday
→ Activity
Schedule technical workshop
→ Task

This gives Quorentra a clean temporal model.


134. Tasks Introduce New Concepts

Task management will require:

due_at
status
priority
assignee
completion

and relationships to:

Company
Contact
Opportunity

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 automation
Recurring tasks
AI task generation
Email reminders
Push notifications
Calendar synchronization
Escalation rules

yet.

Start with:

Create Task
Read Tasks
Complete Task

That is enough for a valuable next increment.


138. Next Article

In Part 23, we will build:

Creating Tasks from ChatGPT — Follow-Ups, Due Dates, Priorities, Assignments, and Completion

We will introduce:

tasks.read
tasks.create
tasks.complete
Task model
TaskDraft
Task Status
├── Open
└── Completed
Task Priority
├── Low
├── Normal
├── High
└── Urgent
Title
Description
Due date
Assignee
Company relationship
Contact relationship
Opportunity relationship
Relative date resolution
Entity resolution
Assignment resolution
Missing-field detection
Task validation
Task confirmation
Prepared Task mutations
Idempotent Task creation
Task AuditEvents
Task completion
Completion confirmation
Open task lists
Overdue task queries
Today's tasks
Company tasks
Contact tasks
Opportunity tasks
Task widgets

Our target interaction will be:

User:
"Remind me to call Sarah Johnson at
Adventure Works Friday about the revised
Azure migration pricing."

Quorentra will structure:

Task
Title:
Call Sarah Johnson
Due:
Friday
Contact:
Sarah Johnson
Company:
Adventure Works
Opportunity:
Azure Migration
Description:
Discuss revised pricing.
Status:
Open
Priority:
Normal

After confirmation:

Create Task
Open Task List

Later:

User:
"I called Sarah. Mark the task complete.
She asked us to send the revised proposal
next 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:

Company
Contact
Opportunity
Activity
Task

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.

Quorentra

Contact

Menu

Designed with WordPress

Discover more from Quorentra

Subscribe now to keep reading and get access to the full archive.

Continue reading