Series: Building Quorentra CRM from Zero — A Modular, ChatGPT-Native AI CRM
Building conversational CRM task management with follow-ups, due dates, priorities, assignments, entity relationships, confirmation, completion, audit, and daily work views.

1. Introduction
Quorentra now knows both the structure and history of customer relationships.
Our core CRM entities are:
CompanyContactOpportunity
Activity
which lets Quorentra remember:
CallsEmailsMeetingsNotes
For example:
Record that I called Sarah Johnson at Adventure Works today about the Azure migration. She asked for revised pricing.
Quorentra can now convert that statement into an authoritative Activity attached to:
Sarah Johnson ↓Adventure Works ↓Azure Migration
and place it on the relevant customer timelines.
That answers:
What happened?
But CRM work does not stop there.
After Sarah requests revised pricing, someone needs to do something.
For example:
Remind me to call Sarah Johnson at Adventure Works Friday about the revised Azure migration pricing.
This describes a future obligation.
It is not an Activity.
It is a:
Task
In Part 23, we build Quorentra’s first conversational Task Management module.
By the end of this article, users will be able to:
Create TasksView TasksFind Today's TasksFind Overdue TasksAssociate Tasks with CRM entitiesAssign TasksComplete Tasks
through ChatGPT.
This moves Quorentra another step forward.
It no longer only knows:
Who?What?What happened?
It also begins tracking:
What needs to happen next?
2. The CRM Time Model
Part 22 introduced an important distinction:
PAST / COMPLETED ↓ ActivityFUTURE / REQUIRED ↓ Task
Examples:
Called Sarah today→ ActivityCall Sarah Friday→ Task
Another:
Sent proposal yesterday→ ActivitySend revised proposal tomorrow→ Task
Another:
Met customer Monday→ ActivitySchedule architecture workshop→ Task
This distinction gives Quorentra a clean temporal model.
3. Our Target Interaction
The user says:
Remind me to call Sarah Johnson at Adventure Works Friday about the revised Azure migration pricing.
ChatGPT extracts:
Intent:Create TaskAction:CallContact:Sarah JohnsonCompany:Adventure WorksOpportunity:Azure MigrationDue:FridayDescription:Discuss revised pricing.
Quorentra resolves the referenced CRM entities and the relative due date.
The user then sees:
┌──────────────────────────────────────────────┐│ QUORENTRA ││ Create Task ││ ││ Call Sarah Johnson ││ ││ Due ││ Friday, 7 August 2026 ││ ││ Contact ││ Sarah Johnson ││ ││ Company ││ Adventure Works ││ ││ Opportunity ││ Azure Migration ││ ││ Priority ││ Normal ││ ││ Discuss revised pricing. ││ ││ [Cancel] [Create Task] │└──────────────────────────────────────────────┘
Only after explicit confirmation is the Task created.
4. Why Tasks Matter
CRM systems frequently fail at one of their most important jobs:
Helping users remember what they need to do.
Customer relationships generate obligations constantly:
Call customerSend proposalReview pricingPrepare demoSchedule meetingProvide documentationFollow upAsk for decisionRenew contractReview legal comments
If those obligations remain only in:
MemoryEmailSticky notesCalendarChat
the CRM is incomplete.
Quorentra needs a structured future-work model.
5. Tasks Make ChatGPT Operational
Once Tasks exist, users can ask:
What do I need to do today?
What is overdue?
What do I need to do this week?
What do I need to follow up on for Adventure Works?
Do I have anything outstanding with Sarah Johnson?
What tasks are related to the Azure Migration opportunity?
These are extremely practical CRM questions.
6. Keep the First Task Module Small
Do not immediately build:
Recurring TasksTask TemplatesDependenciesSubtasksKanban BoardsEscalationSLA ManagementNotificationsEmail RemindersCalendar SyncWorkflow AutomationAI Task Generation
Those are useful later.
For the MVP we need:
CreateReadComplete
That gives us a usable task lifecycle.
7. Starting Checkpoint
Before implementing Part 23, verify Part 22.
Run:
cd backendpython -m pytest
Then:
cd ..\chatgpt-uinpm run build
Test:
Record that I called Sarah Johnson at Adventure Works today about the Azure migration. She asked for revised pricing.
Verify:
Activity Draft ↓Entity Resolution ↓Preparation ↓Confirmation ↓Activity Creation ↓Timeline
We will use that interaction as the starting context for our first Task.
8. Define Task Permissions
Introduce:
tasks.readtasks.createtasks.complete
Later:
tasks.updatetasks.deletetasks.assign
may become separate capabilities.
For the MVP:
ReadCreateComplete
are sufficient.
9. Example Role Matrix
| Role | Read | Create | Complete |
|---|---|---|---|
| Viewer | ✓ | — | — |
| Member | ✓ | ✓ | ✓ |
| Manager | ✓ | ✓ | ✓ |
| Admin | ✓ | ✓ | ✓ |
| Owner | ✓ | ✓ | ✓ |
Later assignment rules can become more sophisticated.
10. Define Task Status
Start with only:
class TaskStatus(str, Enum): OPEN = "open" COMPLETED = "completed"
Do not begin with:
OPENIN_PROGRESSBLOCKEDWAITINGDEFERREDCANCELLEDCOMPLETEDARCHIVED
unless the MVP actually needs them.
11. Why Only Open and Completed?
Because the fundamental Task question is:
Does this still need to happen?
For the first implementation:
Yes→ OpenNo, it was done→ Completed
That is enough.
12. Define Task Priority
Introduce:
class TaskPriority(str, Enum): LOW = "low" NORMAL = "normal" HIGH = "high" URGENT = "urgent"
Default:
NORMAL
unless the user explicitly indicates otherwise.
13. Do Not Infer Urgency Casually
User:
Call Sarah Friday.
Do not decide:
priority = high
because the Opportunity is valuable.
That is an inference.
Default:
priority = normal
User:
Urgently call Sarah tomorrow.
Now:
priority = urgent
is supported by explicit language.
14. Define the Task Domain
Our initial Task model might contain:
idorganization_idtitledescriptionstatusprioritydue_atassigned_to_user_idcompany_idcontact_idopportunity_idcreated_by_user_idcompleted_atcompleted_by_user_idversioncreated_atupdated_at
This is enough for the first Task module.
15. Required Fields
For Task creation, require:
titledue_atassigned_to_user_id
and at least one CRM relationship:
company_idORcontact_idORopportunity_id
For a CRM Task, this keeps future work connected to customer context.
16. Should Description Be Required?
No.
For:
Call Sarah Friday.
the title:
Call Sarah Johnson
may be enough.
Description is optional.
This keeps Task capture lightweight.
17. Assignment Is Important
Someone needs to own the Task.
The simplest conversational request is:
Remind me to call Sarah Friday.
Here:
assigned_to_user_id=current authenticated user
The word:
me
maps naturally to the authenticated user.
18. Never Let ChatGPT Invent User IDs
The model may supply:
assignee_reference ="me"
or:
assignee_reference ="Alex"
But Quorentra resolves that reference.
ChatGPT must never supply:
assigned_to_user_id =UUID(...)
as authoritative identity.
19. Default Assignee
For the MVP, if no assignee is explicitly given:
assigned_to_user_id =current user
This supports natural requests such as:
Follow up with Adventure Works Friday.
without requiring:
Assign the task to me.
20. Explicit Assignment
Later the user may say:
Ask Michael to send the revised proposal Friday.
Quorentra can resolve:
Michael ↓Organization Member ↓assigned_to_user_id
But only within the active tenant.
21. Tenant-Scoped Assignment
Never search globally for:
Michael
Task assignment must resolve only:
active organization ↓active members
A user from another tenant must never become an assignee.
22. Ambiguous Assignee
Suppose the organization contains:
Michael BrownMichael Smith
User:
Assign it to Michael.
Do not choose.
Return an ambiguity result.
ChatGPT can ask:
Which Michael do you mean: Michael Brown or Michael Smith?
23. Define Field Ownership
| Field | Source |
|---|---|
id | System |
organization_id | TenantContext |
title | User / conservative derivation |
description | User |
status | System |
priority | User / default |
due_at | Server-resolved |
assigned_to_user_id | Server-resolved |
company_id | Entity resolution |
contact_id | Entity resolution |
opportunity_id | Entity resolution |
created_by_user_id | Authenticated user |
completed_at | System |
completed_by_user_id | Authenticated user |
version | System |
created_at | System |
updated_at | System |
24. Status Is System-Controlled
During creation:
status =OPEN
ChatGPT should not create:
status =COMPLETED
through the normal creation path.
If the user describes something already completed, it probably belongs in:
Activity
instead.
25. Temporal Intent Classification
Compare:
Call Sarah tomorrow.
This means:
Task
Compare:
I called Sarah yesterday.
This means:
Activity
Compare:
I need to call Sarah.
This likely means:
Task
possibly with a missing due date.
Temporal interpretation is now an important part of Quorentra’s conversational interface.
26. Introduce TaskDraft
Reuse our established draft architecture.
Conceptually:
TaskDraft├── id├── organization_id├── user_id│├── title├── description├── priority├── due_at├── due_expression│├── assignee_reference├── assigned_to_user_id│├── company_reference├── company_id│├── contact_reference├── contact_id│├── opportunity_reference├── opportunity_id│├── status├── field_sources├── dependency_status├── workflow_id├── expires_at├── created_at└── updated_at
Again:
TaskDraft ≠ Task
It is temporary conversational state.
27. Why Use a Task Draft?
User:
Remind me to call Sarah about the pricing.
Quorentra may know:
Action:Call SarahDescription:Discuss pricing
but still need:
Which Sarah?When?
The draft preserves the known information while those gaps are resolved.
28. Task Draft Status
Reuse:
incompletecompletepreparedcancelledexpiredconverted
No new lifecycle is required.
29. Relative Due Dates
Users naturally say:
tomorrowFridaynext Mondaythis afternoonend of the weeknext week15 August
These must become authoritative timestamps.
30. Resolve Relative Dates Server-Side
Suppose today is:
Sunday, 2 August 2026
User:
Call Sarah Friday.
Resolve:
Friday, 7 August 2026
according to the organization’s/user’s timezone and date-resolution policy.
31. Do Not Store “Friday”
The Task should store:
due_at =2026-08-07T...
not:
due_at ="Friday"
The original expression can optionally remain in draft provenance.
32. Date Without Time
Suppose:
Call Sarah Friday.
The user did not specify a time.
We need a deterministic policy.
For the MVP, define a default task due time such as:
17:00 local time
or an organization-level configured default.
The important requirement is consistency.
33. Better Long-Term Model
Eventually we may distinguish:
Due Date
from:
Due Date + Time
But the MVP can use one timestamp if the defaulting policy is explicit.
Do not overcomplicate the first Task implementation.
34. Explicit Time
User:
Call Sarah Friday at 10 AM.
Resolve directly:
Friday, 7 August 202610:00Europe/Amsterdam
and store the corresponding UTC timestamp.
35. Due Date Must Be Future-Oriented
Unlike Activities, Tasks generally represent future work.
If:
due_at < now
at creation time, this may be valid if the user intentionally creates an already-overdue Task.
For example:
Add a task that was due yesterday to send the proposal.
This is valid.
So unlike Activity future-date validation, we should not reject past due dates.
Instead:
due_at < nowANDstatus = OPEN
means:
Overdue
36. Overdue Is Derived
Do not necessarily store:
status =OVERDUE
For the MVP.
Derive:
is_overdue =status == OPENANDdue_at < now
This avoids state synchronization problems.
37. Task Title Extraction
User:
Remind me to call Sarah Johnson Friday about revised pricing.
A reasonable title is:
Call Sarah Johnson
Description:
Discuss revised pricing.
This is conservative structuring.
38. Do Not Generate Elaborate Titles
Avoid turning it into:
Strategic Follow-Up Call Regarding Revised Azure MigrationCommercial Pricing Proposal
unless the user actually provided that meaning.
Short operational titles are better.
39. Create Task Draft Tool
Introduce:
create_task_draft
Conceptually:
class CreateTaskDraftInput(BaseModel): title: str | None = None description: str | None = None due_expression: str | None = None priority: TaskPriority | None = None assignee_reference: str | None = None company_reference: str | None = None contact_reference: str | None = None opportunity_reference: str | None = None
Notice again what is missing:
organization_idassigned_to_user_idcompany_idcontact_idopportunity_idcreated_by_user_idstatusversion
Those are controlled by Quorentra.
40. Draft Creation Flow
Receive Explicit Facts ↓Interpret Task Intent ↓Normalize Title ↓Resolve Due Date ↓Resolve Assignee ↓Resolve Company ↓Resolve Contact ↓Resolve Opportunity ↓Cross-Validate Relationships ↓Determine Missing Fields ↓Create TaskDraft ↓Return Structured Result
No Task exists yet.
41. Entity Resolution Reuses Part 22
We already built resolution for:
CompanyContactOpportunity
Do not build a new resolver for Tasks.
Reuse the same application services.
This is the payoff from the modular architecture.
42. Company-Scoped Contact Resolution
User:
Call Sarah Johnson at Adventure Works Friday.
Resolve:
Adventure Works ↓company_id ↓Sarah Johnson within Company ↓contact_id
This reduces ambiguity.
43. Opportunity Resolution
User:
Call Sarah Friday about the Azure migration.
If Sarah uniquely resolves to Adventure Works:
Sarah Johnson ↓Adventure Works
then search:
Azure MigrationWHERE company_id = AdventureWorks.id
This creates a coherent relationship graph.
44. Relationship Cross-Validation
Suppose:
Sarah Johnson→ Adventure WorksContoso Renewal→ Contoso
The user accidentally references both.
Return:
ENTITY_RELATIONSHIP_CONFLICT
Do not create the Task.
45. Derive Company
As with Activities:
Contact ↓Company
and:
Opportunity ↓Company
can supply the Company relationship.
This avoids unnecessary questions.
46. Task Relationship Examples
Valid:
Company only✓Contact + Company✓Opportunity + Company✓Contact + Opportunity + Company✓
Derived:
Contact only→ derive CompanyOpportunity only→ derive Company
Invalid for the CRM Task MVP:
No CRM relationship
47. Why Require CRM Context?
Quorentra is a CRM, not a general-purpose todo application.
A task like:
Buy milk tomorrow.
does not belong in the CRM.
A task like:
Call Sarah at Adventure Works tomorrow.
does.
This boundary keeps the product focused.
48. Missing Fields
User:
Remind me to call Sarah.
Possible known values:
title =Call Sarahassignee =current user
Missing:
Contact identityDue dateCRM relationship
The server returns structured missing fields.
49. Example Draft Response
{ "status": "incomplete", "values": { "title": "Call Sarah", "priority": "normal", "assigned_to": "current_user" }, "missing_required_fields": [ "contact", "due_at" ]}
ChatGPT can ask for the missing information.
50. Multi-Turn Completion
User:
Sarah Johnson at Adventure Works.
Update:
contact =Sarah Johnsoncompany =Adventure Works
Still missing:
due_at
ChatGPT asks:
When should you call Sarah?
User:
Friday.
The draft becomes complete.
51. One-Shot Completion
User:
Remind me to call Sarah Johnson at Adventure Works Friday about the revised Azure migration pricing.
Everything required is present.
Quorentra should proceed directly to preparation.
No unnecessary follow-up questions.
52. Priority Extraction
User:
Urgently call Sarah tomorrow about the contract.
Extract:
priority =urgent
User:
Call Sarah tomorrow.
Default:
priority =normal
53. Priority Correction
User:
Actually, make that high priority.
Update:
TaskDraft.priority =high
If already prepared, invalidate the existing MutationRequest.
Require fresh confirmation.
54. Update Task Draft
Introduce:
update_task_draft
Conceptually:
class UpdateTaskDraftInput(BaseModel): draft_id: UUID title: str | None = None description: str | None = None due_expression: str | None = None priority: TaskPriority | None = None assignee_reference: str | None = None company_reference: str | None = None contact_reference: str | None = None opportunity_reference: str | None = None
Every update revalidates relevant fields.
55. Field Provenance
Example:
{ "field_sources": { "title": "model_structured_from_user", "description": "user", "due_at": "server_resolved_relative_time", "priority": "server_default", "assigned_to_user_id": "server_current_user", "contact_id": "server_entity_resolution", "company_id": "server_derived_relationship", "opportunity_id": "server_entity_resolution" }}
This becomes increasingly useful as conversational workflows become richer.
56. Prepare Task Creation
Once the TaskDraft is complete:
prepare_task_creation
Input:
{ "draft_id": "..."}
57. Preparation Flow
Load TaskDraft ↓Verify Tenant ↓Verify User ↓Verify tasks.create ↓Verify Draft Not Expired ↓Resolve/Revalidate Assignee ↓Resolve/Revalidate Entities ↓Validate Relationships ↓Validate Due Date ↓Validate Title ↓Create MutationRequest ↓Mark Draft Prepared ↓Return Confirmation
Still:
0 Task rows created
58. Mutation Type
Use:
mutation_type =task.create
Example proposed values:
{ "title": "Call Sarah Johnson", "description": "Discuss revised Azure migration pricing.", "priority": "normal", "due_at": "2026-08-07T15:00:00Z", "assigned_to_user_id": "...", "company_id": "...", "contact_id": "...", "opportunity_id": "..."}
59. Task Confirmation Widget
Create:
chatgpt-ui/src/tasks/└── TaskCreationConfirmation.tsx
Example:
┌──────────────────────────────────────────────┐│ QUORENTRA ││ Create Task ││ ││ Call Sarah Johnson ││ ││ Due ││ Friday, 7 August 2026 ││ ││ Priority ││ Normal ││ ││ Assigned to ││ You ││ ││ Sarah Johnson ││ Adventure Works ││ Azure Migration ││ ││ Discuss revised pricing. ││ ││ [Cancel] [Create Task] │└──────────────────────────────────────────────┘
60. Confirmation Shows the Real Due Date
If the user said:
Friday
the widget should show:
Friday, 7 August 2026
The user confirms the resolved meaning, not the ambiguous relative phrase.
61. Confirmation Shows Assignment
This becomes especially important when Tasks can be assigned to colleagues.
For example:
Assigned to:Michael Brown
The user should know exactly who will own the Task.
62. Confirmation Shows CRM Context
Show:
ContactCompanyOpportunity
when present.
This allows the user to catch incorrect entity resolution.
63. Execute Task Creation
Introduce:
create_task
Input:
{ "mutation_request_id": "..."}
No arbitrary Task fields are accepted.
64. Execution Flow
Load MutationRequest ↓Verify Tenant ↓Verify User ↓Verify tasks.create ↓Verify Pending ↓Verify Not Expired ↓Load TaskDraft ↓Verify Draft Matches ↓Revalidate Assignee ↓Revalidate CRM Entities ↓Revalidate Relationships ↓Revalidate Due Date ↓Create Task ↓Create AuditEvent ↓Mark Mutation Executed ↓Mark TaskDraft Converted ↓Commit
65. Transaction Boundary
Use:
BEGINcreate Taskcreate AuditEventmark MutationRequest executedmark TaskDraft convertedCOMMIT
On failure:
ROLLBACK
66. Task Initial State
Every newly created Task begins:
status =OPENcompleted_at =NULLcompleted_by_user_id =NULLversion =1
67. Task Audit Event
Use:
action =task.created
Store appropriate metadata:
organization_idactor_user_identity_idnew_valuesmutation_request_idworkflow_idinvocation_sourcecreated_at
68. Authoritative Task Result
After commit:
{ "success": true, "task": { "id": "...", "title": "Call Sarah Johnson", "description": "Discuss revised Azure migration pricing.", "status": "open", "priority": "normal", "due_at": "2026-08-07T15:00:00Z", "assigned_to": { "id": "...", "display_name": "Ben" }, "company": { "id": "...", "name": "Adventure Works" }, "contact": { "id": "...", "name": "Sarah Johnson" }, "opportunity": { "id": "...", "name": "Azure Migration" }, "version": 1 }}
Only now may ChatGPT say:
The task has been created.
69. Task Success Widget
┌──────────────────────────────────────────────┐│ QUORENTRA ││ Task Created ││ ││ Call Sarah Johnson ││ ││ Friday, 7 August 2026 ││ Normal Priority ││ ││ Adventure Works ││ Azure Migration ││ ││ [View Task] [View My Tasks] │└──────────────────────────────────────────────┘
70. Read Task Tool
Introduce:
get_task
The server returns one authoritative Task.
This supports:
Show me the task to call Sarah.
Search first if needed, then retrieve the selected Task.
71. Search Tasks
Introduce:
search_tasks
Useful filters:
statuspriorityassigned_tocompany_referencecontact_referenceopportunity_referencedue_fromdue_tooverduelimit
This is the basis for conversational work queues.
72. My Open Tasks
User:
Show my open tasks.
Quorentra queries:
organization_id =active tenantassigned_to_user_id =current userstatus =open
ordered by:
due_at ASC
73. Example Task List
MY OPEN TASKSToday──────────────────────────────────09:00 Send Contoso proposalHigh14:00 Review Fabrikam pricingNormalFriday──────────────────────────────────17:00 Call Sarah JohnsonAdventure WorksAzure MigrationNormal
This is immediately useful.
74. Today’s Tasks
Introduce a convenient query:
get_my_tasks_today
or implement it through structured search_tasks.
The important behavior is:
current user+open+due during local today
75. Timezone Matters Again
“Today” must be evaluated in the user’s or organization’s timezone.
Do not calculate it purely in UTC.
Otherwise late-evening and early-morning Tasks may appear on the wrong day.
76. Overdue Tasks
User:
What is overdue?
Query:
status = openANDdue_at < now
for the relevant assignee.
77. Example Overdue Result
OVERDUE31 JulSend revised security questionnaireContoso2 days overdue1 AugCall Lisa ChenAdventure Works1 day overdue
Again:
overdue
is derived.
The stored Task remains:
status = open
78. Tasks This Week
User:
What do I need to do this week?
Resolve the user’s local week boundaries.
Query:
status = openassigned_to = current userdue_at within week
Then sort by due date and priority.
79. Company Tasks
User:
What is outstanding for Adventure Works?
Resolve:
Adventure Works ↓company_id
then query open Tasks associated with that Company.
80. Contact Tasks
User:
Do I have anything to do with Sarah Johnson?
Resolve Sarah within Company context if available.
Return open Tasks linked to her.
81. Opportunity Tasks
User:
What follow-ups are open for the Azure Migration opportunity?
Resolve the Opportunity and return associated Tasks.
This makes Opportunity detail much more operational.
82. Company Detail Evolves Again
Company detail can now include:
Adventure WorksContacts3Opportunities2Recent Activity4Open Tasks3
This creates a more complete customer workspace.
83. Contact Detail Evolves
Sarah Johnson’s Contact detail can show:
Recent Activity02 Aug — CallCustomer requested revised pricing.Open Tasks07 Aug — Call Sarah JohnsonDiscuss revised pricing.
Now we can see:
what happened+what happens next
in one place.
84. Opportunity Detail Evolves
Azure Migration can show:
Value€90,000StageProposalRecent Activity02 Aug — Customer requested revised pricing.Open Tasks07 Aug — Call Sarah Johnson.
This begins to resemble a practical sales workspace.
85. Completing Tasks
Creation alone is not enough.
The user needs to say:
Mark the Sarah call task complete.
This is an authoritative mutation.
Therefore it should follow the same governed mutation architecture.
86. Resolve the Task
First identify which Task the user means.
Search:
titlecontactcompanyopportunitystatus = open
If exactly one strong match exists:
task_id
is resolved.
87. Ambiguous Task
Suppose there are two open Tasks:
Call Sarah Johnson7 AugCall Sarah Johnson14 Aug
User:
Mark the Sarah call complete.
Do not choose arbitrarily.
ChatGPT should ask which Task.
88. Prepare Task Completion
Introduce:
prepare_task_completion
Input:
{ "task_id": "...", "expected_version": 1}
The server verifies:
TenantPermissionTask stateAssignee policyVersion
and creates:
mutation_type =task.complete
89. Completion Confirmation
Render:
┌──────────────────────────────────────────────┐│ QUORENTRA ││ Complete Task ││ ││ Call Sarah Johnson ││ ││ Adventure Works ││ Azure Migration ││ ││ Due ││ Friday, 7 August 2026 ││ ││ Mark this task as completed? ││ ││ [Cancel] [Complete Task] │└──────────────────────────────────────────────┘
90. Execute Completion
Introduce:
complete_task
Input:
{ "mutation_request_id": "..."}
Execution flow:
Load MutationRequest ↓Verify Tenant ↓Verify User ↓Verify tasks.complete ↓Verify Pending ↓Verify Not Expired ↓Load Task ↓Verify Expected Version ↓Verify Task Still Open ↓Set Status Completed ↓Set completed_at ↓Set completed_by_user_id ↓Increment Version ↓Create AuditEvent ↓Execute MutationRequest ↓Commit
91. Completed Task State
After execution:
status =completedcompleted_at =server timestampcompleted_by_user_id =authenticated userversion =2
92. Completion Audit Event
Use:
action =task.completed
Store:
old_valuesnew_valuesactortask_idmutation_request_idworkflow_id
93. Idempotent Completion
Calling:
complete_task
twice with the same MutationRequest must not:
increment version twicecreate duplicate AuditEventschange completed_at twice
The second call returns the logical result of the already executed request.
94. Completing an Already Completed Task
If a new completion attempt targets a Task that is already completed, return structured state:
{ "code": "TASK_ALREADY_COMPLETED", "task_id": "...", "completed_at": "..."}
Do not treat this as a new mutation.
95. Optimistic Concurrency
Task completion should use:
expected_version
If the Task changed after preparation:
expected_version = 1actual_version = 2
reject execution.
Require fresh state.
96. Why Concurrency Matters
Imagine:
User A prepares completionUser B edits/completes TaskUser A confirms old state
Without version checking, User A may overwrite newer state.
The mutation architecture should prevent this.
97. Task Completion Is Not Automatically an Activity
This is an important design decision.
Completing:
Call Sarah
only tells us:
Task completed
It does not tell us:
Call actually happenedWhat was discussedWhat the outcome was
Therefore do not automatically create a Call Activity merely because the Task was completed.
98. But ChatGPT Can Compose Both
Consider:
I called Sarah. Mark the task complete. She asked us to send the revised proposal next Tuesday.
This contains three business operations:
1. Complete Task2. Record Call Activity3. Create New Follow-Up Task
This is where our modular architecture becomes powerful.
99. Decompose the User Intent
The request becomes:
"I called Sarah." ↓Activity"Mark the task complete." ↓Task Completion"Send revised proposal next Tuesday." ↓New Task
Each operation uses an existing governed primitive.
100. Do Not Build a Giant Special Endpoint
Avoid:
complete_task_record_activity_and_create_followup()
That creates tightly coupled business logic.
Instead compose:
prepare_task_completionprepare_activity_creationprepare_task_creation
inside a higher-level conversational workflow.
101. Workflow Composition
Conceptually:
User Intent ↓Conversation Planner ↓Workflow├── Task Completion├── Activity Creation└── Task Creation
Each step retains:
ValidationAuthorizationConfirmationAuditIdempotency
This is a much stronger architecture.
102. Shared Workflow ID
All three mutations can share:
workflow_id
For example:
Workflow:Sarah revised pricing follow-up├── task.completed├── activity.created└── task.created
This gives us traceability across the composed operation.
103. Should We Confirm All Three Separately?
For the first implementation, separate confirmations are safest.
But this introduces friction.
Later we can build:
Composite Confirmation
showing:
This will:✓ Complete "Call Sarah Johnson"✓ Record today's call Customer requested revised proposal.✓ Create follow-up task Send revised proposal Due Tuesday
Then:
[Cancel] [Confirm All]
That is a future enhancement.
104. Keep Part 23 Focused
For this article, fully implement:
Create TaskRead TasksComplete Task
Define composition behavior conceptually.
Do not yet build a general workflow engine.
105. Task List Widget
Create:
chatgpt-ui/src/tasks/├── TaskList.tsx├── TaskCard.tsx├── TaskDetail.tsx├── TaskCreationConfirmation.tsx└── TaskCompletionConfirmation.tsx
106. Task Card
Example:
┌────────────────────────────────────────────┐│ Call Sarah Johnson ││ ││ Adventure Works ││ Azure Migration ││ ││ Due 7 Aug ││ Normal ││ ││ ○ Open │└────────────────────────────────────────────┘
107. High Priority Task
┌────────────────────────────────────────────┐│ Send revised proposal ││ ││ Adventure Works ││ Azure Migration ││ ││ Due Tomorrow ││ High ││ ││ ○ Open │└────────────────────────────────────────────┘
108. Completed Task
┌────────────────────────────────────────────┐│ Call Sarah Johnson ││ ││ Adventure Works ││ Azure Migration ││ ││ ✓ Completed ││ 7 Aug 2026 │└────────────────────────────────────────────┘
109. Task List Grouping
For user-facing task views, group naturally:
OverdueTodayTomorrowThis WeekLater
This makes ChatGPT’s task widget useful as a lightweight work dashboard.
110. Do Not Make the Model Calculate Groups
The backend should return authoritative date information.
The frontend can group Tasks deterministically using the user’s timezone.
Avoid relying on ChatGPT to manually classify hundreds of Tasks.
111. Database Indexes
Useful indexes include:
organization_id, assigned_to_user_id, status, due_atorganization_id, company_id, status, due_atorganization_id, contact_id, status, due_atorganization_id, opportunity_id, status, due_at
These support common work-list queries.
112. Open Task Query
The most common query will likely be:
WHERE organization_id = :organization_idAND assigned_to_user_id = :user_idAND status = 'open'ORDER BY due_at ASC
This should be efficient.
113. Pagination
Task lists can grow.
Support:
limitcursor
or an equivalent pagination strategy.
Do not send thousands of Tasks into ChatGPT context.
114. Search Result Limits
For conversational queries, a default such as:
20
is usually sufficient.
The user can request more if necessary.
115. Task Draft Tests
Test:
create draftload own draftupdate draftcancel draftexpire draftconvert draftcross-user access deniedcross-tenant access denied
116. Task Status Tests
Verify:
new Task→ opencompleted Task→ completed
Unsupported states should fail.
117. Priority Tests
Verify:
lownormalhighurgent
Default:
normal
Unknown priority:
rejected
118. Due Date Tests
Use a fixed test clock.
Test:
tomorrowFridaynext Monday15 AugustFriday at 10 AM
Verify exact resolved timestamps.
119. Past Due Creation Test
User intentionally creates:
Add a task due yesterday to send the proposal.
Expected:
Task created as openis_overdue = true
Do not silently move the date.
120. Assignee Tests
Test:
no assignee→ current user"me"→ current userunique member→ member resolvedambiguous member→ clarification requiredexternal user→ rejectedcross-tenant member→ rejected
121. Company Resolution Tests
Verify:
explicit CompanyCompany derived from ContactCompany derived from Opportunityambiguous Companycross-tenant Company
122. Contact Resolution Tests
Verify:
unique ContactCompany-scoped Contactambiguous Contactcross-tenant Contact
123. Opportunity Resolution Tests
Verify:
unique OpportunityCompany-scoped Opportunityambiguous Opportunitycross-tenant Opportunity
124. Relationship Conflict Test
Attempt:
Sarah Johnson→ Adventure WorksContoso Renewal→ Contoso
Expected:
ENTITY_RELATIONSHIP_CONFLICT
No Task created.
125. Missing Field Tests
Draft:
Call Sarah JohnsonFriday
with Sarah resolved.
Expected:
complete
because:
assignee =current user
and Company can be derived.
126. Missing Due Date
Draft:
Call Sarah Johnson
Expected:
missing_required_fields =["due_at"]
127. Preparation Tests
Verify:
incomplete draft cannot prepareexpired draft cannot prepareinvalid assignee cannot preparerelationship conflict cannot preparecomplete draft can preparepreparation creates no Task
128. Edit-After-Prepare Test
Prepare:
Due:Friday
Then:
Actually make it Monday.
Expected:
TaskDraft updatedold MutationRequest invalidatedfresh preparation required
129. Creation Permission Tests
Viewer:
create_task_draft → deniedprepare_task_creation → deniedcreate_task → denied
Authorized Member:
allowed
130. Permission Change Before Execution
Prepare a Task.
Remove:
tasks.create
before execution.
Expected:
create_task → denied
No Task created.
131. Task Creation Transaction Test
Successful execution must:
create exactly one Taskcreate exactly one AuditEventexecute MutationRequestconvert TaskDraft
inside one transaction.
132. Task Creation Idempotency Test
Execute the same MutationRequest twice.
Expected:
1 Task1 AuditEvent
Never:
2 Tasks
133. Completion Permission Test
A user without:
tasks.complete
cannot complete a Task.
This is checked both at preparation and execution.
134. Completion Version Test
Prepare completion at:
version = 1
Modify the Task.
Now:
version = 2
Attempt old completion.
Expected:
VERSION_CONFLICT
135. Completion Transaction Test
Successful completion must atomically:
set statusset completed_atset completed_by_user_idincrement versioncreate AuditEventexecute MutationRequest
136. Completion Idempotency Test
Execute completion twice.
Expected:
one logical completionone completion AuditEvent
137. My Tasks Query Test
Ensure:
Tenant A User A
only sees Tasks:
organization_id = Tenant Aassigned_to_user_id = User A
No cross-user or cross-tenant leakage.
138. Today’s Tasks Test
Use a fixed local timezone.
Verify Tasks at:
00:0112:0023:59
are correctly included.
UTC boundary handling must be correct.
139. Overdue Query Test
Create:
Open Task due yesterdayCompleted Task due yesterdayOpen Task due tomorrow
Expected overdue results:
Open Task due yesterday
only.
140. Company Task Query Test
An Adventure Works Task must appear in:
Adventure Works open tasks
but not in:
Contoso open tasks
141. Contact Task Query Test
A Task linked to Sarah must appear in:
Sarah Johnson open tasks
142. Opportunity Task Query Test
A Task linked to Azure Migration must appear in:
Azure Migration open tasks
143. Logging
Useful structured operational fields:
task_draft_idtask_idorganization_iduser_idassigned_to_user_idworkflow_idmutation_request_idmutation_typeresultduration_ms
Avoid unnecessarily logging:
full task descriptioncustomer detailspersonal data
144. Metrics
Useful metrics include:
task_drafts_created_totaltasks_created_totaltasks_completed_totaltask_creation_failures_totaltask_completion_failures_totaltask_resolution_conflicts_totaltask_confirmation_cancellations_total
Later these can feed Quorentra’s operational dashboards.
145. Version Update
Part 23 introduces:
Task DomainTask DraftsTask CreationTask AssignmentDue DatesPrioritiesOpen Task ListsOverdue DetectionTask Completion
Update:
app/core/constants.py
from:
APP_VERSION = "0.9.0"
to:
APP_VERSION = "0.10.0"
146. Quorentra 0.10.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 ✓└── Tasks ✓Activity Types├── Calls ✓├── Emails ✓├── Meetings ✓└── Notes ✓Task Management├── Open Tasks ✓├── Due Dates ✓├── Priorities ✓├── Assignments ✓├── Overdue Detection ✓└── Completion ✓Sales├── Pipeline ✓├── Opportunity Stages ✓├── Probability ✓└── Weighted Pipeline ✓Interfaces├── REST ✓├── MCP ✓└── ChatGPT ✓ChatGPT UI├── Pipeline ✓├── Company Detail ✓├── Contact Detail ✓├── Opportunity Detail ✓├── Activity Timeline ✓├── Task List ✓├── Task Detail ✓├── Mutation Confirmation ✓├── Company Creation ✓├── Contact Creation ✓├── Opportunity Creation ✓├── Activity Creation ✓├── Task Creation ✓└── Task Completion ✓Mutation Governance├── Mutation Requests ✓├── Confirmation ✓├── Expiry ✓├── Cancellation ✓├── Idempotency ✓├── Transactions ✓├── Optimistic Concurrency ✓└── Audit Events ✓Conversational Operations├── Company Drafts ✓├── Contact Drafts ✓├── Opportunity Drafts ✓├── Activity Drafts ✓├── Task Drafts ✓├── Entity Resolution ✓├── Member Resolution ✓├── Relative Time Resolution ✓├── Relationship Validation ✓├── Missing-Field Detection ✓├── Field Provenance ✓├── Multi-Turn Completion ✓├── Draft Correction ✓├── Prepared Mutations ✓└── Safe Execution ✓CRM Memory├── Company Timeline ✓├── Contact Timeline ✓├── Opportunity Timeline ✓├── Calls ✓├── Emails ✓├── Meetings ✓└── Notes ✓CRM Work Management├── My Tasks ✓├── Today's Tasks ✓├── Overdue Tasks ✓├── Company Tasks ✓├── Contact Tasks ✓└── Opportunity Tasks ✓Workflow Composition├── Company → Contact ✓├── Company → Opportunity ✓├── Contact → Activity ✓├── Opportunity → Activity ✓├── Company → Task ✓├── Contact → Task ✓├── Opportunity → Task ✓├── Workflow Correlation ✓└── Multi-Mutation Composition ◐ChatGPT CRM Operations├── Read Pipeline ✓├── Read Companies ✓├── Read Contacts ✓├── Read Opportunities ✓├── Read Activities ✓├── Read Tasks ✓├── Create Company ✓├── Create Contact ✓├── Create Opportunity ✓├── Update Opportunity Stage ✓├── Record Activity ✓├── Create Task ✓└── Complete Task ✓AI Intelligence -
147. Acceptance Criteria
Part 23 is complete when:
✓ Part 22 regression suite remains green✓ tasks.read exists✓ tasks.create exists✓ tasks.complete exists✓ Viewer cannot create or complete Tasks✓ authorized users can create Tasks✓ authorized users can complete Tasks✓ Task model exists✓ Task migration applies✓ Task is tenant-scoped✓ Task records creator✓ Task records assignee✓ Task version starts at 1✓ OPEN status exists✓ COMPLETED status exists✓ new Tasks start OPEN✓ LOW priority exists✓ NORMAL priority exists✓ HIGH priority exists✓ URGENT priority exists✓ NORMAL is the default✓ urgency is not invented✓ title is required✓ due_at is required✓ assignee is required✓ description is optional✓ at least one CRM relationship is required✓ organization_id comes from TenantContext✓ created_by_user_id comes from authentication✓ ChatGPT cannot supply authoritative tenant✓ ChatGPT cannot supply authoritative user IDs✓ no explicit assignee defaults to current user✓ "me" resolves to current user✓ named organization member can resolve✓ ambiguous member requires clarification✓ cross-tenant member cannot resolve✓ TaskDraft exists✓ TaskDraft is tenant-scoped✓ TaskDraft is user-scoped✓ TaskDraft supports expiry✓ TaskDraft supports cancellation✓ TaskDraft supports conversion✓ TaskDraft supports workflow_id✓ create_task_draft exists✓ update_task_draft exists✓ draft operations create no Task✓ relative due-date resolution works✓ tomorrow resolves correctly✓ Friday resolves correctly✓ explicit dates resolve correctly✓ explicit times resolve correctly✓ timezone handling works✓ default time policy is deterministic✓ relative expression is not authoritative persisted time✓ past due dates are permitted✓ past-due open Tasks derive overdue state✓ OVERDUE does not need to be stored as Task status✓ Company resolution works✓ Contact resolution works✓ Opportunity resolution works✓ Contact resolution can use Company context✓ Opportunity resolution can use Company context✓ Company can be derived from Contact✓ Company can be derived from Opportunity✓ entity relationships are cross-validated✓ conflicting relationships are rejected✓ cross-tenant entities never resolve✓ Task title extraction is conservative✓ description preserves user meaning✓ unsupported facts are not invented✓ field provenance is recorded✓ missing-field detection is server-owned✓ incomplete drafts remain drafts✓ complete requests do not trigger unnecessary questions✓ multi-turn completion works✓ correction-before-confirmation works✓ prepare_task_creation exists✓ incomplete Task cannot prepare✓ expired Task cannot prepare✓ invalid assignee cannot prepare✓ relationship conflict cannot prepare✓ preparation creates no Task✓ task.create MutationRequest exists✓ MutationRequest contains exact proposed values✓ editing prepared TaskDraft invalidates old confirmation✓ confirmation expiry works✓ Task creation confirmation widget exists✓ title is visible✓ resolved due date is visible✓ priority is visible✓ assignee is visible✓ Company is visible when present✓ Contact is visible when present✓ Opportunity is visible when present✓ description is visible when present✓ Cancel creates no Task✓ create_task accepts mutation_request_id only✓ execution rechecks tenant✓ execution rechecks user✓ execution rechecks permission✓ execution rechecks request state✓ execution rechecks expiry✓ execution rechecks assignee✓ execution rechecks entities✓ execution rechecks relationships✓ Task creation is transactional✓ Task ID is server-generated✓ Task starts OPEN✓ Task version starts at 1✓ AuditEvent is transactional✓ MutationRequest becomes executed✓ TaskDraft becomes converted✓ failed Task transaction rolls back✓ duplicate execution is idempotent✓ one MutationRequest creates at most one Task✓ authoritative Task result is returned✓ ChatGPT reports success only after execution✓ success widget uses authoritative Task data✓ get_task exists✓ search_tasks exists✓ My Tasks query works✓ Today's Tasks query works✓ Overdue Tasks query works✓ Company Tasks query works✓ Contact Tasks query works✓ Opportunity Tasks query works✓ Task queries are tenant-scoped✓ assignee queries are user-scoped where required✓ date queries use correct timezone✓ task lists are bounded✓ pagination exists✓ open Tasks sort by due date✓ prepare_task_completion exists✓ completion requires tasks.complete✓ ambiguous Task requires clarification✓ already-completed Task is handled safely✓ completion confirmation widget exists✓ complete_task accepts mutation_request_id✓ completion rechecks tenant✓ completion rechecks user✓ completion rechecks permission✓ completion rechecks Task status✓ completion uses optimistic concurrency✓ completion sets COMPLETED✓ completion sets completed_at✓ completion sets completed_by_user_id✓ completion increments version✓ completion creates task.completed AuditEvent✓ completion is transactional✓ completion is idempotent✓ completing a Task does not automatically invent an Activity✓ Activity + Task workflows can share workflow_id✓ architecture supports future multi-mutation composition✓ Quorentra reports version 0.10.0
Most importantly:
A user can now describe future customer work naturally, allow Quorentra to resolve the people, companies, opportunities, assignees, and dates involved, review the structured Task, explicitly confirm it, and manage that Task through completion.
148. What We Have Achieved
Quorentra can now understand:
Remind me to call Sarah Johnson at Adventure Works Friday about the revised Azure migration pricing.
and transform it into:
Natural Language ↓Task Intent ↓Call Sarah Johnson ↓Resolve Sarah ↓Resolve Adventure Works ↓Resolve Azure Migration ↓Resolve Friday ↓Assign Current User ↓Task Draft ↓Validate ↓Prepare ↓Confirmation ↓Create Task ↓Audit ↓Open Work Queue
That is an important product milestone.
149. Quorentra Now Understands Past and Future
We now have:
CRM TIME
Past Future
│ │
▼ ▼
Activity Task
│ │
▼ ▼
What happened? What must happen?
Combined with:
CompanyContactOpportunity
Quorentra now has a useful operational CRM model.
150. The CRM Graph Is Growing
Our business graph now looks like:
Company
/ | \
/ | \
▼ ▼ ▼
Contact Opportunity Task
│ │ ▲
│ │ │
└────┬───┘ │
│ │
▼ │
Activity │
│ │
└───────────┘
More precisely, both Activities and Tasks can reference:
CompanyContactOpportunity
This gives ChatGPT rich business context without requiring a monolithic CRM architecture.
151. A Daily CRM Workflow Is Now Possible
A user can begin the day by asking:
What do I need to do today?
Quorentra can answer from authoritative Task data.
The user completes a call:
I called Sarah. Mark the task complete.
Quorentra completes the Task.
The user adds:
She asked for a revised proposal.
Quorentra records the Activity.
Then:
Remind me to send it Tuesday.
Quorentra creates the next Task.
The resulting CRM history becomes:
Adventure Works│├── Activity│ 02 Aug│ Sarah requested revised pricing.│├── Task│ 07 Aug│ Call Sarah│ ✓ Completed│├── Activity│ 07 Aug│ Discussed revised pricing.│└── Task 11 Aug Send revised proposal ○ Open
This is a genuine CRM workflow.
152. ChatGPT Is Becoming the Work Interface
Traditional CRM interaction often looks like:
Open CRM ↓Find Account ↓Open Contact ↓Find Opportunity ↓Open Activity Form ↓Fill Fields ↓Save ↓Open Task Form ↓Fill Fields ↓Save
Quorentra can increasingly support:
I spoke with Sarah about the Azure migration. She wants revised pricing. Remind me to send it Tuesday.
That is a fundamentally different interaction model.
153. But ChatGPT Is Not the Database
This distinction remains essential.
ChatGPT handles:
LanguageIntentConversationExplanation
Quorentra handles:
IdentityAuthorizationValidationRelationshipsStateTransactionsAuditPersistence
The architecture is therefore:
ChatGPT │ │ natural interaction ▼Quorentra Tools │ │ governed operations ▼Quorentra Domain │ │ authoritative state ▼PostgreSQL
This separation is what makes a ChatGPT-native CRM viable.
154. The Next Missing Capability
We can now track:
CompaniesContactsOpportunitiesActivitiesTasks
But sales work also revolves around:
Meetings
We currently represent a completed meeting as:
Activity Type = Meeting
That is correct for history.
But future meetings require more structure.
A scheduled customer meeting needs:
Start timeEnd timeParticipantsCompanyOpportunityLocationMeeting linkAgendaStatus
That is more than a generic Task.
155. Why Meetings Deserve Their Own Module
Consider:
Schedule a meeting with Sarah Johnson at Adventure Works next Tuesday at 2 PM to review the Azure migration proposal.
That contains:
Meeting ↓Start Time ↓Duration ↓Participants ↓Company ↓Opportunity ↓Agenda
A Task cannot represent all of this cleanly.
156. Meeting Lifecycle
A future Meeting may move through:
Scheduled ↓Completed
or:
Scheduled ↓Cancelled
Once completed, it may generate:
Meeting Activity
with:
SummaryNotesOutcomesFollow-Up Tasks
This creates another natural bridge between future work and historical CRM memory.
157. Keep Meeting Integration Local First
We should not immediately connect:
Google CalendarMicrosoft OutlookMicrosoft TeamsZoomGoogle Meet
First build Quorentra’s internal Meeting domain.
Then external calendar systems can become adapters around a stable domain model.
That is the modular approach.
158. Next Article
Scheduling Meetings from ChatGPT — Participants, Dates, Agendas, CRM Context, and Meeting Lifecycle
We will introduce:
meetings.readmeetings.createmeetings.cancelmeetings.completeMeeting modelMeetingDraftMeeting Status├── Scheduled├── Completed└── CancelledTitleAgendaStart timeEnd timeDurationTimezoneOrganizerParticipantsCompany relationshipContact relationshipsOpportunity relationshipRelative date resolutionParticipant resolutionEntity resolutionMeeting validationConflict detectionMeeting confirmationPrepared Meeting mutationsIdempotent Meeting creationMeeting cancellationMeeting completionMeeting AuditEventsUpcoming meetingsToday's meetingsCompany meetingsContact meetingsOpportunity meetingsMeeting widgets
Our target interaction will be:
User:"Schedule a meeting with Sarah Johnson atAdventure Works next Tuesday at 2 PM toreview the Azure migration proposal."
Quorentra will resolve:
Sarah Johnson ↓ContactAdventure Works ↓CompanyAzure Migration ↓OpportunityNext Tuesday at 2 PM ↓Authoritative Timestamp
and construct:
MeetingTitle:Azure Migration Proposal ReviewDate:Tuesday, 11 August 2026Time:14:00Participant:Sarah JohnsonCompany:Adventure WorksOpportunity:Azure MigrationAgenda:Review the Azure migration proposal.
After confirmation:
Create Meeting ↓Upcoming Meetings
Later:
User:"The meeting with Sarah is finished.We agreed to send the revised proposalFriday."
Quorentra can eventually compose:
Complete Meeting ↓Create Meeting Activity ↓Create Follow-Up Task
At that point our modular CRM workflow becomes:
Company │ ├── Contact │ ├── Opportunity │ ├── Activity │ ↑ │ │ completed interaction │ │ ├── Task │ └── Meeting │ └── future customer interaction
Each module remains independently governed and testable.
ChatGPT provides the conversational layer that composes them naturally.
That is the architecture we want for Quorentra — A Modular, ChatGPT-Native AI CRM.