Series: Building Quorentra CRM from Zero — A Modular, ChatGPT-Native AI CRM
Building safe conversational Company creation with entity resolution, duplicate prevention, normalization, confirmation, audit, and chained Company-to-Opportunity workflows.

1. Introduction
In Part 19, Quorentra learned how to create Opportunities conversationally.
A user could say:
Create a €120,000 Microsoft Copilot Deployment opportunity for Contoso closing November 30, 2026.
Quorentra could transform that request into:
Natural Language ↓Structured Opportunity Draft ↓Company Resolution ↓Business Defaults ↓Validation ↓Confirmation ↓Opportunity Creation ↓Audit
But the workflow depended on one important assumption:
The Company already existed.
If the user said:
Create a €90,000 Azure migration opportunity for Adventure Works.
and Adventure Works did not exist, Quorentra had to stop.
That is too restrictive for a useful CRM.
A CRM should help users capture new business relationships, not require them to manually prepare every dependency beforehand.
In Part 20, we remove that limitation.
We will teach Quorentra how to:
Resolve CompanyDetect Missing CompanyPropose Company CreationCollect Missing InformationDetect DuplicatesNormalize Company DataValidateConfirmCreate CompanyAuditResume Original Opportunity Workflow
This introduces something more significant than another CRUD operation.
It introduces our first:
Dependency-aware conversational CRM workflow.
2. The Target Experience
Consider this request:
Create a €90,000 Azure migration opportunity for Adventure Works closing December 15, 2026.
ChatGPT extracts:
Company:Adventure WorksOpportunity:Azure MigrationAmount:€90,000Expected Close:15 December 2026
Quorentra searches the current tenant.
Result:
Company not found:Adventure Works
Instead of abandoning the workflow, ChatGPT can respond:
Adventure Works isn’t currently in Quorentra. Would you like to create the company first?
The user answers:
Yes.
Quorentra prepares:
┌──────────────────────────────────────────┐│ QUORENTRA ││ Create Company ││ ││ Company ││ Adventure Works ││ ││ This will create a new Company record. ││ ││ [Cancel] [Create Company] │└──────────────────────────────────────────┘
After confirmation:
Adventure Works
is created.
But the workflow does not end there.
Quorentra remembers why the Company was needed.
It resumes:
Azure Migration€90,00015 December 2026
and prepares the Opportunity.
The user then confirms:
┌──────────────────────────────────────────┐│ QUORENTRA ││ Create Opportunity ││ ││ Company ││ Adventure Works ││ ││ Opportunity ││ Azure Migration ││ ││ Value ││ €90,000 ││ ││ Expected Close ││ 15 December 2026 ││ ││ [Cancel] [Create Opportunity] │└──────────────────────────────────────────┘
We have moved from a single mutation to a business workflow.
3. Why Company Creation Matters
Companies are foundational CRM entities.
They become anchors for:
ContactsOpportunitiesActivitiesTasksMeetingsNotesDocumentsCommunicationsAI knowledge
Without Company creation, our ChatGPT interface can only operate on organizations already stored in Quorentra.
With Company creation, users can begin building CRM data conversationally.
For example:
Add Northwind as a customer.
Create Fabrikam in the CRM.
We have a new prospect called Alpine Ski House.
Add Adventure Works and create a €90,000 Azure migration opportunity.
The last example is particularly important.
It contains multiple business intentions in one natural-language request.
4. We Are Not Building Generic CRUD
A tempting implementation would expose:
create_company(...)
directly to ChatGPT and let the model supply whatever fields it wants.
That would work technically.
But it would undermine the architecture we have deliberately built.
Quorentra’s principle remains:
ChatGPT expresses intent. Quorentra governs business state.
Therefore Company creation follows the same lifecycle introduced in Parts 18 and 19:
Draft ↓Validate ↓Prepare ↓Confirm ↓Execute ↓Audit
5. Company Creation Is Simpler Than Opportunity Creation
Opportunity creation required fields such as:
CompanyNameAmountCurrencyStageProbabilityExpected Close Date
A minimal Company can be much simpler.
For the MVP, we can define:
Required:nameOptional:websiteindustryphonecountrydescription
This makes Company creation conversationally lightweight.
6. Minimal Does Not Mean Uncontrolled
Although only:
name
may be mandatory, Company creation still introduces important risks:
Duplicate CompaniesSpelling VariationsDomain VariationsTenant LeakageIncorrect NormalizationAI-Invented DetailsRepeated Execution
We must solve these systematically.
7. Starting Checkpoint
Before beginning Part 20, verify Part 19.
From:
backend
run:
python -m pytest
Then build the ChatGPT UI:
cd ..\chatgpt-uinpm run build
Verify:
Create a €120,000 Microsoft Copilot Deployment opportunity for Contoso closing November 30, 2026.
Confirm that Quorentra can:
Resolve ContosoCreate Opportunity DraftApply DefaultsValidatePrepareConfirmCreate OpportunityCreate AuditEventReturn Authoritative Result
Then try:
Create a €90,000 Azure migration opportunity for Adventure Works closing December 15, 2026.
Assuming Adventure Works does not exist, Quorentra should currently stop at Company resolution.
That is the limitation Part 20 removes.
8. Define the Company Creation Permission
Add:
companies.create
This remains separate from:
companies.readcompanies.updatecompanies.delete
Our permission model now begins to look like:
Companies├── companies.read├── companies.create└── companies.updateOpportunities├── opportunities.read├── opportunities.create└── opportunities.update
9. Example Role Matrix
For the MVP:
| Role | Read Company | Create Company | Update Company |
|---|---|---|---|
| Viewer | ✓ | — | — |
| Member | ✓ | ✓ | ✓ |
| Manager | ✓ | ✓ | ✓ |
| Admin | ✓ | ✓ | ✓ |
| Owner | ✓ | ✓ | ✓ |
The important point is not the exact matrix.
It is that:
Read ≠ Create ≠ Update
10. Define the Company Domain
Our Company model might contain:
idorganization_idnamenormalized_namewebsitedomainindustryphonecountrydescriptionversioncreated_atupdated_at
Some fields are user-facing.
Others exist to support reliable application behavior.
11. Classify the Fields
Before creating anything conversationally, classify ownership.
| Field | Source |
|---|---|
id | System |
organization_id | TenantContext |
name | User |
normalized_name | Server |
website | User |
domain | Server-derived |
industry | User |
phone | User |
country | User |
description | User |
version | System |
created_at | System |
updated_at | System |
Again:
ChatGPT does not control system-owned fields.
12. Never Accept Tenant ID from ChatGPT
The tool must not accept:
{ "organization_id": "..."}
The authenticated:
TenantContext
determines the organization.
This rule should now feel repetitive.
That is good.
Security architecture should be repetitive and predictable.
13. Introduce CompanyDraft
As with Opportunities, we should not immediately create an authoritative Company.
Introduce:
CompanyDraft
Conceptually:
CompanyDraft├── id├── organization_id├── user_id├── name├── normalized_name├── website├── domain├── industry├── phone├── country├── description├── status├── field_sources├── expires_at├── created_at└── updated_at
14. Why Use a Draft for Such a Simple Entity?
If Company only requires a name, a draft might initially appear excessive.
But it gives us consistency.
All conversational creation can follow:
Draft→ Prepare→ Confirm→ Execute
More importantly, Company creation may later collect:
WebsiteIndustryCountryPhoneDescription
The draft architecture gives us room to grow without changing the interaction model.
15. Company Draft Status
Use:
incompletecompletepreparedcancelledexpiredconverted
If Company name is the only required field, most drafts will immediately become:
complete
That is perfectly acceptable.
16. Draft Expiry
Company drafts should expire similarly to Opportunity drafts.
For example:
expires_at =last_updated_at + 24 hours
The exact duration should eventually become configurable.
17. Create the Company Draft Model
Conceptually:
class CompanyDraft(Base): __tablename__ = "company_drafts" id = ... organization_id = ... user_id = ... name = ... normalized_name = ... website = ... domain = ... industry = ... phone = ... country = ... description = ... status = ... field_sources = ... expires_at = ... created_at = ... updated_at = ...
18. Keep the Authoritative Company Strict
As before:
CompanyDraft
may be incomplete.
But:
Company
must satisfy all domain rules.
Do not weaken the authoritative schema to accommodate conversational input.
19. Company Name Is Required
For the MVP:
name
is mandatory.
A request such as:
Create a new company.
is incomplete.
ChatGPT should ask:
What is the company called?
It must not invent one.
20. Company Name Extraction
If the user says:
Add Adventure Works to the CRM.
ChatGPT can extract:
name = "Adventure Works"
This is user-provided information.
It is not AI-generated data.
21. Company Name Normalization
Company names often appear in different forms:
Adventure Worksadventure worksAdventure Works ADVENTURE WORKS
We should create:
normalized_name
server-side.
A simple MVP normalization can:
trim whitespacecollapse repeated spacesconvert to lowercase
So:
" Adventure Works "
becomes:
"adventure works"
22. Preserve the Display Name
Do not replace the actual Company name with the normalized value.
Store:
name =Adventure Works
and:
normalized_name =adventure works
The first is for users.
The second supports matching.
23. Do Not Over-Normalize Legal Names
Be cautious about automatically removing:
LtdLLCIncB.V.GmbHS.A.PLC
For example:
Contoso B.V.
and:
Contoso GmbH
may be distinct legal entities.
The MVP should not assume otherwise.
24. Duplicate Detection Begins with Exact Normalized Name
Before Company creation, search:
organization_id+normalized_name
If:
Adventure Works
already exists in the tenant, Quorentra should not blindly create another one.
25. Example Exact Duplicate
Existing:
Adventure Works
User:
Add adventure works.
Normalization:
adventure works
Existing normalized name:
adventure works
Result:
Potential exact duplicate
26. What Should Quorentra Do?
For an exact normalized-name match, the MVP should usually block automatic duplicate creation.
Return:
Company already exists.
along with the existing Company identity.
ChatGPT can then say:
Adventure Works already exists in Quorentra. I’ll use the existing company.
This becomes especially valuable for chained workflows.
27. Duplicate Detection Is Tenant-Scoped
Tenant A may have:
Adventure Works
Tenant B may also have:
Adventure Works
That is completely valid.
Duplicate detection must always include:
organization_id
28. Website as Optional Input
The user may say:
Add Adventure Works with website adventure-works.com.
Then:
website
can be captured.
But website remains optional for the MVP.
29. Normalize Websites
Users may provide:
adventure-works.comwww.adventure-works.comhttps://adventure-works.comhttps://www.adventure-works.com/
We should normalize these server-side.
For example, authoritative website storage might become:
https://adventure-works.com
according to a defined normalization policy.
30. Extract the Domain
From:
https://www.adventure-works.com
derive:
domain =adventure-works.com
server-side.
ChatGPT should not calculate authoritative domain values.
31. Why Domain Matters
Domains provide another strong duplicate signal.
Suppose the tenant contains:
Adventure Works Internationaladventure-works.com
The user asks:
Create Adventure Works with adventure-works.com.
Even if normalized names differ, the domain match strongly suggests an existing Company.
32. Domain Duplicate Check
Our duplicate logic can therefore consider:
Exact normalized nameORExact normalized domain
This remains deterministic.
No embeddings are required.
33. Domain Collision Example
Existing:
Name:Adventure Works InternationalDomain:adventure-works.com
New draft:
Name:Adventure WorksDomain:adventure-works.com
Return:
Potential existing Company detected.
Do not automatically create another record.
34. Website Validation
If the user supplies:
not a website
as a website, reject it structurally.
Return something like:
{ "field": "website", "code": "INVALID_WEBSITE", "message": "Website must be a valid HTTP or HTTPS URL."}
ChatGPT can ask the user to correct it.
35. Industry
industry remains optional.
The user might say:
Add Adventure Works, a manufacturing company.
Then:
industry = manufacturing
can be extracted.
But do not infer an industry from the Company name alone.
36. Do Not Automatically Research the Company Yet
It may be tempting to have ChatGPT search the web and populate:
IndustryCountryPhoneWebsiteEmployee countRevenue
automatically.
That can come later.
For the MVP:
Company creation uses user-provided facts, deterministic normalization, and explicit business defaults—not uncontrolled enrichment.
This keeps the data lineage clear.
37. Country
Country is optional.
User:
Add Contoso Netherlands.
That does not necessarily mean:
country = Netherlands
unless the user’s wording explicitly indicates that as a fact.
Natural-language interpretation should remain conservative.
38. Phone
Phone remains optional.
If supplied, normalize it according to a defined policy.
Eventually we may use:
E.164
format.
For the MVP, basic validation is sufficient.
39. Description
Description is optional.
If the user says:
Adventure Works is a new prospect interested in Azure modernization.
that sentence can become:
description
if the user clearly intends it as Company context.
Do not invent a corporate profile.
40. Field Provenance
Reuse the provenance concept introduced in Part 19.
For example:
{ "field_sources": { "name": "user", "website": "user", "domain": "server_derived", "industry": "user" }}
This becomes increasingly useful as conversational data entry expands.
41. Create Company Draft Tool
Introduce:
create_company_draft
Input:
class CreateCompanyDraftInput(BaseModel): name: str | None = None website: str | None = None industry: str | None = None phone: str | None = None country: str | None = None description: str | None = None
Notice what is missing:
organization_idnormalized_namedomainversioncreated_atupdated_at
Those belong to Quorentra.
42. Draft Creation Flow
The service performs:
Receive Explicit Fields ↓Normalize Name ↓Normalize Website ↓Derive Domain ↓Validate Fields ↓Detect Missing Fields ↓Check Duplicate Signals ↓Create Draft ↓Return Structured Result
No Company is created.
43. Example Draft Result
User:
Add Adventure Works.
Result:
{ "draft_id": "...", "status": "complete", "values": { "name": "Adventure Works", "website": null, "domain": null, "industry": null, "phone": null, "country": null, "description": null }, "missing_fields": [], "duplicate_matches": []}
Because only Company name is required, the draft is immediately complete.
44. Missing Name Example
User:
Add a new company.
Result:
{ "status": "incomplete", "missing_fields": [ "name" ]}
ChatGPT asks:
What is the company called?
45. Update Company Draft
Introduce:
update_company_draft
Input:
class UpdateCompanyDraftInput(BaseModel): draft_id: UUID name: str | None = None website: str | None = None industry: str | None = None phone: str | None = None country: str | None = None description: str | None = None
Every update triggers:
NormalizationValidationDuplicate RecheckMissing-Field Recalculation
46. Duplicate Results Should Be Structured
Instead of returning:
Duplicate company
return:
{ "duplicate_matches": [ { "company_id": "...", "name": "Adventure Works International", "website": "https://adventure-works.com", "match_reason": "domain" } ]}
This gives ChatGPT enough information to explain the conflict.
47. Exact Match Versus Possible Match
We can distinguish:
EXACT_NAMEEXACT_DOMAIN
Later:
SIMILAR_NAME
may be added.
For the MVP, deterministic exact signals are sufficient.
48. Do Not Use AI Similarity Yet
Avoid introducing:
EmbeddingsSemantic searchLLM duplicate classification
into Company creation now.
Our goal is a running modular MVP.
Start with:
Normalized NameDomain
These provide significant protection at low complexity.
49. Existing Company Resolution
Suppose the user says:
Create an opportunity for adventure works.
Before creating a Company draft, Quorentra should search existing Companies.
If an exact normalized Company already exists:
Adventure Works
return that Company.
Do not create a draft unnecessarily.
50. Resolution Before Creation
The workflow should therefore be:
Company Reference ↓Resolve Existing Company ↓Found? ┌────┴────┐Yes No │ │Use ID ▼ Offer Creation
Creation is a fallback.
Not the default.
51. Why “Offer Creation”?
Suppose:
Create an opportunity for Adventure Works.
No exact Company exists.
Quorentra should not automatically create one without telling the user.
Creating a Company is an authoritative CRM mutation.
Therefore ChatGPT asks:
Adventure Works isn’t currently in Quorentra. Would you like to create it?
The user explicitly decides.
52. Company Creation Confirmation
Once the draft is complete and duplicate checks pass, call:
prepare_company_creation
Input:
{ "draft_id": "..."}
The server performs all authoritative preparation.
53. Preparation Flow
Load Draft ↓Verify Tenant ↓Verify User ↓Verify companies.create ↓Verify Not Expired ↓Revalidate Fields ↓Recheck Duplicates ↓Create MutationRequest ↓Mark Draft Prepared ↓Return Confirmation Data
No Company row exists yet.
54. Mutation Type
Use:
mutation_type =company.create
Proposed values may contain:
{ "name": "Adventure Works", "website": null, "domain": null, "industry": null, "phone": null, "country": null, "description": null}
55. Confirmation UI
Create:
chatgpt-ui/src/mutations/└── CompanyCreationConfirmation.tsx
Example:
┌──────────────────────────────────────────┐│ QUORENTRA ││ Create Company ││ ││ Company ││ Adventure Works ││ ││ Website ││ — ││ ││ Industry ││ — ││ ││ This will create a new CRM Company. ││ ││ [Cancel] [Create Company] │└──────────────────────────────────────────┘
56. Show Only Useful Fields
We do not need to show:
normalized_namedomainorganization_idversion
unless they help the user understand the operation.
The confirmation UI should focus on business-facing data.
57. Optional Fields Should Be Visible When Present
If the user said:
Add Adventure Works with website adventure-works.com, manufacturing industry, Netherlands.
Then confirmation might show:
CompanyAdventure WorksWebsiteadventure-works.comIndustryManufacturingCountryNetherlands
This gives the user an opportunity to catch interpretation errors.
58. Confirmation Must Remain Explicit
The button should say:
Create Company
not:
Continue
or:
OK
The business side effect should be unmistakable.
59. Execute Company Creation
Introduce:
create_company
Input:
{ "mutation_request_id": "..."}
Again, the execution tool does not accept arbitrary Company fields.
60. Execution Flow
Load MutationRequest ↓Verify Tenant ↓Verify User ↓Verify companies.create ↓Verify Pending ↓Verify Not Expired ↓Load Draft ↓Verify Draft Still Matches ↓Revalidate ↓Recheck Duplicates ↓Create Company ↓Create AuditEvent ↓Mark Mutation Executed ↓Mark Draft Converted ↓Commit
61. Recheck Duplicates at Execution
Why?
Because between:
Prepare
and:
Execute
another user may create:
Adventure Works
If we only checked duplicates during preparation, we could still create an avoidable duplicate.
62. Race Conditions
Suppose User A and User B both try to create:
Adventure Works
simultaneously.
Both drafts may initially see:
No duplicate
We need database-level protection for exact normalized-name duplication if our domain policy forbids it.
63. Unique Constraint
For the MVP, we can consider:
UNIQUE ( organization_id, normalized_name)
if Quorentra’s business rule is:
A tenant cannot have two Companies with exactly the same normalized name.
This is much more reasonable than enforcing unique Opportunity names.
64. But What About Real Companies with the Same Name?
It is possible for distinct legal entities to share similar or even identical names.
Therefore this is a product decision.
A safer MVP policy may be:
Exact normalized name→ block by default→ allow deliberate override later
This protects ordinary users from accidental duplicates while leaving room for advanced handling.
65. Domain Uniqueness
Do not automatically make:
domain
globally unique.
Large organizations may have:
Parent CompanySubsidiaryBusiness Unit
sharing a domain.
Instead, treat exact domain match as a strong warning.
66. Transaction Boundary
Company creation should execute inside:
BEGINcreate Companycreate AuditEventmark MutationRequest executedmark CompanyDraft convertedCOMMIT
On failure:
ROLLBACK
67. Initial Version
The Company starts with:
version = 1
This makes it compatible with future optimistic concurrency control.
68. Company Audit Event
Use:
action =company.created
The event contains:
organization_idactor_user_identity_idnew_valuesinvocation_sourcemutation_request_idcreated_at
69. Example Audit Event
{ "action": "company.created", "entity_type": "company", "entity_id": "...", "new_values": { "name": "Adventure Works", "website": null, "industry": null, "country": null }, "invocation_source": "chatgpt_model"}
70. Authoritative Company Result
After commit:
{ "success": true, "company": { "id": "...", "name": "Adventure Works", "website": null, "domain": null, "industry": null, "phone": null, "country": null, "version": 1 }}
Only now does the Company exist.
71. Success UI
Render:
┌──────────────────────────────────────────┐│ QUORENTRA ││ Company Created ││ ││ Adventure Works ││ ││ CRM Company created successfully. ││ ││ [View Company] │└──────────────────────────────────────────┘
For a standalone Company creation request, the workflow can finish here.
But our more interesting case has an unfinished Opportunity request.
72. Introduce Workflow Continuation
Recall the original request:
Create a €90,000 Azure migration opportunity for Adventure Works closing December 15, 2026.
We already captured:
Opportunity Name:Azure MigrationAmount:€90,000Expected Close:15 December 2026
The only missing dependency was:
company_id
Once Adventure Works is created, Quorentra should resume that draft.
73. Do Not Ask the User to Start Again
A poor experience would be:
Adventure Works has been created. Please tell me the opportunity details again.
The user already supplied them.
The system should preserve valid intent.
74. Opportunity Draft Dependency
Extend:
OpportunityDraft
so it can represent an unresolved Company reference.
For example:
company_id = NULLcompany_reference = "Adventure Works"
and:
missing_dependency =company
This allows the Opportunity draft to exist before its Company exists.
75. Why Preserve the Text Reference?
Because:
Adventure Works
was explicitly provided by the user.
The draft can retain that reference while waiting for entity resolution.
Once the Company is created:
company_id = new_company.id
and the unresolved reference is satisfied.
76. Dependency State
We can introduce:
dependency_status
with values such as:
resolvedunresolvedwaiting_for_creationfailed
For the MVP, this may be stored explicitly or derived.
The important architectural concept is that drafts can wait for another business entity.
77. Chained Workflow Architecture
Our first chained workflow becomes:
User Intent ↓Opportunity Draft ↓Resolve Company ↓Company Missing ↓Ask User ↓Company Draft ↓Prepare Company ↓Confirm Company ↓Create Company ↓Attach Company ID to Opportunity Draft ↓Revalidate Opportunity Draft ↓Prepare Opportunity ↓Confirm Opportunity ↓Create Opportunity
This is a major architectural milestone.
78. Two Confirmations or One?
Could we show one confirmation:
Create Company + Opportunity
and execute both together?
Eventually, yes.
But for the MVP, keep them separate:
Confirm CompanythenConfirm Opportunity
This preserves the simple mutation architecture we already trust.
79. Why Separate Confirmations?
Because they are two authoritative business effects:
Create CompanyCreate Opportunity
If the user wants the Opportunity but not the Company creation, the dependency cannot be satisfied.
Keeping the effects explicit makes the workflow easier to understand, test, and audit.
80. Future Composite Mutations
Later we could introduce:
WorkflowRequest
containing:
Create CompanyCreate Primary ContactCreate OpportunityCreate Follow-Up Task
with one confirmation.
But that would require:
Cross-entity transaction semanticsPartial failure policyWorkflow-level idempotencyWorkflow-level auditCompensation behavior
That is beyond the MVP.
81. Resume Context
After successful Company creation, the server should return enough structured information to resume the dependency.
For example:
{ "success": true, "company": { "id": "...", "name": "Adventure Works" }, "continuation": { "type": "opportunity_draft", "draft_id": "..." }}
The client does not need to reconstruct the original request.
82. Server-Owned Continuation
This is important.
Avoid storing the entire continuation only in:
ChatGPT memory
Instead, the relevant Opportunity draft already exists in Quorentra.
The continuation simply points to:
draft_id
83. Attach Company to Opportunity Draft
After Company creation:
OpportunityDraft.company_id
becomes:
new_company.id
Then Quorentra reruns:
ValidationMissing-field detectionDuplicate awareness
84. Complete Opportunity Draft
Our original draft already had:
nameamountcurrencyexpected_close_date
and defaults supplied:
stageprobability
Therefore once:
company_id
is resolved:
missing_fields = []
The draft becomes:
complete
85. Prepare Opportunity Automatically?
After Company creation, should Quorentra immediately call:
prepare_opportunity_creation
?
Yes, if:
OpportunityDraft
is now complete and valid.
Preparation has no authoritative CRM side effect.
It merely creates a confirmation request.
That is safe.
86. Do Not Automatically Execute the Opportunity
The Company confirmation does not implicitly confirm the Opportunity.
Therefore:
Create Company
does not mean:
Create Opportunity
The Opportunity still receives its own explicit confirmation.
87. Full User Experience
User:
Create a €90,000 Azure migration opportunity for Adventure Works closing December 15, 2026.
ChatGPT:
Adventure Works isn’t currently in Quorentra. Would you like to create the company first?
User:
Yes.
Widget:
Create CompanyAdventure Works
User confirms.
Quorentra:
Adventure Works created.
Then immediately:
Create OpportunityAzure MigrationAdventure Works€90,000Qualification20%15 December 2026
User confirms.
Quorentra:
Opportunity created.
The original intent has been fulfilled.
88. Why This Feels ChatGPT-Native
The user did not have to:
Open CompaniesClick New CompanyEnter NameSaveReturn to OpportunitiesClick New OpportunitySearch CompanyEnter AmountSelect StageSelect DateSave
Instead:
One Natural-Language Request+Two Explicit Confirmations
completed the business process.
This is exactly the kind of friction reduction a ChatGPT-native CRM should provide.
89. Yet the Backend Became More Rigorous
The conversational simplicity hides substantial governance:
Tenant IsolationRBACEntity ResolutionDraft StateNormalizationValidationDuplicate DetectionConfirmationIdempotencyTransactionsAuditDependency ResolutionContinuation
That is the correct trade-off.
The user experience becomes simpler because the application architecture becomes more disciplined.
90. Cancellation During Company Creation
Suppose the user cancels:
Create Adventure Works
Then:
Company not createdOpportunity Draft remains unresolved
The Opportunity must not be created.
ChatGPT can say:
Company creation was cancelled, so the Opportunity remains as an incomplete draft.
91. What Happens to the Opportunity Draft?
We can keep it until:
expiry
so the user can later say:
Continue the Adventure Works opportunity.
This makes cancellation non-destructive.
92. Company Creation Failure
Suppose Company creation fails because another user created Adventure Works milliseconds earlier.
Instead of failing the whole workflow permanently:
Detect duplicate ↓Resolve newly existing Company ↓Attach it to Opportunity Draft ↓Continue
This is a powerful recovery pattern.
93. Idempotent Dependency Resolution
If the desired Company already exists by execution time, Quorentra can potentially return:
existing_company
instead of creating a duplicate.
But this must be carefully defined.
For the MVP, a duplicate conflict can trigger re-resolution.
94. Re-Resolution Flow
Company Creation Conflict ↓Search Exact Normalized Name ↓Exactly One Match? ┌──────┴───────┐ Yes No │ │Use Existing Ask UserCompany
This makes the workflow robust under concurrency.
95. Audit Both Operations Separately
The chained workflow produces:
AuditEvent 1company.createdAuditEvent 2opportunity.created
Each retains its own:
actortimestampentitymutation_request_id
This is preferable to hiding multiple operations inside one opaque event.
96. Link Related Audit Events Later
A future enhancement could introduce:
workflow_id
shared by both events.
For example:
workflow_id =7b...
Then we could reconstruct:
Company Created ↓Opportunity Created
as one business workflow.
97. Should We Add workflow_id Now?
A lightweight optional:
workflow_id
is worth considering now.
It can be added to:
OpportunityDraftCompanyDraftMutationRequestAuditEvent
without requiring a full workflow engine.
98. Correlation Versus Orchestration
This distinction is useful.
A:
workflow_id
provides correlation.
It does not mean we have built:
Workflow Engine
yet.
We are simply allowing related operations to be traced together.
99. Invocation Source
Continue recording:
invocation_source
such as:
chatgpt_modelchatgpt_widgetrestsystem
For our chained flow, Company creation may originate from:
chatgpt_widget
while preparation originated from:
chatgpt_model
That distinction can be useful operationally.
100. Logging the Chained Workflow
Operational logs might include:
workflow_idopportunity_draft_idcompany_draft_idcompany_mutation_request_idopportunity_mutation_request_idorganization_iduser_idcurrent_stepresultduration_ms
Do not log unnecessary business content.
101. Company Draft Tests
Test:
create Company draftupdate Company draftexpire Company draftcancel Company drafttenant isolationuser ownership
102. Name Normalization Tests
Verify:
"Adventure Works"" adventure works ""ADVENTURE WORKS""Adventure Works"
produce the expected normalized representation.
Preserve the intended display name separately.
103. Website Normalization Tests
Test:
adventure-works.comwww.adventure-works.comhttps://adventure-works.comhttps://www.adventure-works.com/
according to the normalization rules you define.
Verify:
domain =adventure-works.com
where appropriate.
104. Duplicate Tests
Test:
exact normalized nameexact domaindifferent tenant same namedifferent tenant same domainsimilar but non-exact name
Ensure only current-tenant records influence the result.
105. Permission Tests
Viewer:
create_company_draft → deniedprepare_company_creation → deniedcreate_company → denied
Member:
allowed
according to the MVP permission matrix.
106. Permission Change Test
User prepares Company creation.
Their role changes before confirmation.
Execution must recheck:
companies.create
and reject if no longer authorized.
107. Preparation Tests
Verify:
missing name cannot prepareexpired draft cannot prepareduplicate policy enforcedvalid complete draft can preparepreparation creates no Company
108. Edit-After-Prepare Test
Prepare:
Adventure Works
Then edit the draft to:
Adventure Works Europe
The old MutationRequest must be invalidated.
The user must confirm the new Company identity.
109. Execution Tests
Successful execution must:
create exactly one Companyuse active tenantuse confirmed valuesgenerate Company IDset version = 1create AuditEventexecute MutationRequestconvert CompanyDraftreturn authoritative Company
110. Idempotency Test
Execute the same Company MutationRequest twice.
Expected:
1 Company1 AuditEvent1 logical result
Never:
2 Companies
111. Transaction Failure Test
Simulate failure while creating the AuditEvent.
Expected:
Company creation rolled backMutationRequest remains unexecutedCompanyDraft remains unconverted
112. Race Condition Test
Two users prepare:
Adventure Works
simultaneously.
Execute both.
Expected behavior depends on duplicate policy, but accidental exact duplication should not silently occur.
113. Chained Workflow Test
User requests:
Create a €90,000 Azure migration opportunity for Adventure Works closing December 15, 2026.
Adventure Works does not exist.
Expected:
Opportunity Draft createdCompany dependency unresolvedCompany creation offered
114. Chained Workflow Cancellation Test
Cancel Company creation.
Expected:
No CompanyNo OpportunityOpportunity Draft remains unresolved
115. Chained Workflow Success Test
Confirm Company.
Expected:
Company createdCompany AuditEvent createdOpportunityDraft.company_id populatedOpportunity Draft revalidatedOpportunity preparation available
Then confirm Opportunity.
Expected:
Opportunity createdOpportunity AuditEvent createdOriginal user intent fulfilled
116. Cross-Tenant Dependency Test
Tenant A has:
Adventure Works
Tenant B requests:
Create an opportunity for Adventure Works.
Tenant B must not resolve Tenant A’s Company.
Instead:
Company not found in Tenant B
and Tenant B may create its own Adventure Works.
117. Existing Company Workflow
If Adventure Works already exists:
Resolve Company ↓Existing Company ID ↓Opportunity Draft ↓Prepare Opportunity
No Company creation confirmation appears.
The workflow remains efficient.
118. Existing Company with Case Difference
Existing:
Adventure Works
User:
adventure works
Normalization should resolve it.
Do not offer unnecessary Company creation.
119. Domain-Assisted Resolution
Later we can allow:
Create an opportunity for adventure-works.com.
Quorentra can resolve:
domain
to the Company.
This is useful when Company names are ambiguous.
But it is optional for the initial Part 20 implementation.
120. Avoid Hidden Side Effects
One principle now becomes increasingly important:
A tool should not perform unrelated authoritative mutations merely because they make the user’s request easier.
For example:
create_opportunity
should not silently create a missing Company.
Instead:
Company creation
is an explicit operation.
This makes the system understandable.
121. Dependency Resolution Is Not Hidden Mutation
The workflow engine or application layer may determine:
Company requiredCompany missing
and suggest the next action.
That is orchestration.
The actual creation remains:
ExplicitConfirmedAudited
122. ChatGPT as Conversational Orchestrator
ChatGPT’s role is expanding.
It can now coordinate:
User IntentCompany ResolutionCompany CreationOpportunity ContinuationOpportunity Creation
But it still does not own:
PermissionsTenant ScopeBusiness RulesTransactionsAuthoritative State
This separation remains fundamental.
123. The Application Owns Workflow State
Likewise, ChatGPT can decide conversationally:
We need to create the Company first.
But Quorentra stores:
OpportunityDraftCompanyDraftMutationRequestworkflow_iddependency state
This prevents workflow integrity from depending on model memory.
124. Conversation Can Be Interrupted
Imagine the user closes ChatGPT after Company creation but before confirming the Opportunity.
Because the Opportunity draft is stored server-side, we can later support:
Continue my unfinished Adventure Works opportunity.
The system does not need the original conversation transcript to reconstruct the business state.
125. This Is an Important Architectural Property
A ChatGPT-native application should not require:
one uninterrupted chat session
for business workflow integrity.
The conversation is an interface.
The application is the system.
126. Future Contact Creation
Company creation unlocks the next obvious workflow.
User:
Add Sarah Johnson at Adventure Works.
Quorentra will need to:
Resolve Adventure WorksDetect Sarah Johnson duplicatesCollect email/phone if availableCreate Contact DraftConfirmCreate ContactLink Contact to CompanyAudit
If Adventure Works does not exist:
Create Company ↓Resume Contact
The same dependency pattern applies.
127. Future Opportunity + Contact Workflow
Eventually:
Add Sarah Johnson at Adventure Works and create a €90,000 Azure migration opportunity.
could become:
Resolve Company ↓Create Company if needed ↓Create Contact ↓Create Opportunity ↓Associate Contact
That is a real conversational CRM workflow.
128. Future Task Workflow
Then:
Add Sarah Johnson at Adventure Works, create the Azure opportunity, and remind me to call her Friday.
could produce:
Company ↓Contact ↓Opportunity ↓Task
This demonstrates why we are building modular primitives first.
129. Modular Architecture Pays Off
Each business capability remains independent:
Company ModuleContact ModuleOpportunity ModuleTask ModuleActivity Module
ChatGPT can compose them conversationally.
This is much more maintainable than building giant AI-specific endpoints.
130. Avoid execute_user_request
Do not create a backend endpoint such as:
execute_user_request( natural_language)
that allows an LLM to perform arbitrary operations internally.
Instead expose governed domain capabilities:
resolve_companycreate_company_draftprepare_company_creationcreate_companycreate_opportunity_draftprepare_opportunity_creationcreate_opportunity
This keeps the architecture inspectable.
131. AI Composition, Deterministic Execution
A useful summary is:
AI↓CompositionApplication↓Execution
ChatGPT decides how capabilities can satisfy the user’s intent.
Quorentra decides whether each capability is valid and authorized.
132. Company Search Becomes More Important
As Company creation arrives, our Company lookup capability becomes central.
We should support at least:
Exact normalized namePrefix/name searchDomain lookup
All tenant-scoped.
This capability will later support Contacts and Opportunities too.
133. Search Result Limits
Never return an entire tenant’s Company database to ChatGPT.
Use:
querylimit
and return only relevant fields.
For example:
{ "companies": [ { "id": "...", "name": "Adventure Works", "website": "https://adventure-works.com" } ]}
Minimize unnecessary exposure.
134. Company ID Is Still Important Internally
Although users work with:
Adventure Works
Quorentra relationships should use:
company_id
Natural-language labels are for human interaction.
Stable identifiers are for application integrity.
135. Do Not Store Relationships by Company Name
Never make Opportunity persistence depend on:
company_name = "Adventure Works"
as the relationship.
Use:
company_id
with a foreign key.
Names change.
IDs provide stable identity.
136. Renaming Companies Later
Eventually the user may say:
Rename Adventure Works to Adventure Works Europe.
That will become another governed mutation.
Because Opportunities reference:
company_id
they remain attached correctly after the rename.
This demonstrates why relational integrity matters even in an AI-native interface.
137. Version Update
Part 20 introduces:
Company conversational creationCompany normalizationDuplicate preventionDependency-aware continuationChained CRM workflows
Update:
app/core/constants.py
from:
APP_VERSION = "0.6.0"
to:
APP_VERSION = "0.7.0"
138. Quorentra 0.7.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 ✓Sales├── Pipeline ✓├── Opportunity Stages ✓├── Probability ✓└── Weighted Pipeline ✓Interfaces├── REST ✓├── MCP ✓└── ChatGPT ✓ChatGPT UI├── Pipeline Widget ✓├── Opportunity List ✓├── Opportunity Detail ✓├── Mutation Confirmation ✓├── Opportunity Creation ✓├── Company Creation ✓└── Creation Results ✓Mutation Governance├── Mutation Requests ✓├── Confirmation ✓├── Expiry ✓├── Cancellation ✓├── Idempotency ✓├── Transactions ✓├── Optimistic Concurrency ✓└── Audit Events ✓Conversational Creation├── Opportunity Drafts ✓├── Company Drafts ✓├── Entity Resolution ✓├── Missing-Field Detection ✓├── Business Defaults ✓├── Field Provenance ✓├── Company Name Normalization ✓├── Website Normalization ✓├── Domain Extraction ✓├── Duplicate Detection ✓├── Multi-Turn Completion ✓├── Draft Correction ✓├── Prepared Creation ✓└── Safe Execution ✓Workflow Composition├── Dependency Detection ✓├── Missing Company Detection ✓├── Company Creation Continuation ✓├── Opportunity Resume ✓├── Workflow Correlation ✓└── Multi-Entity Workflow ✓ChatGPT CRM Operations├── Read Pipeline ✓├── Read Opportunities ✓├── View Opportunity ✓├── Update Opportunity Stage ✓├── Create Opportunity ✓├── Resolve Company ✓├── Create Company ✓├── Create Contact -├── Create Task -└── Create Activity -Activities -Tasks -AI Intelligence -
139. Acceptance Criteria
Part 20 is complete when:
✓ Part 19 regression suite remains green✓ companies.create permission exists✓ Viewer cannot create Companies✓ authorized roles can create Companies✓ permission is rechecked at execution✓ CompanyDraft model exists✓ CompanyDraft migration applies✓ CompanyDraft is tenant-scoped✓ CompanyDraft is user-scoped✓ CompanyDraft supports expiry✓ CompanyDraft supports cancellation✓ CompanyDraft supports conversion✓ Company name is required✓ optional Company fields remain optional✓ system fields cannot be supplied by ChatGPT✓ Company name normalization exists✓ display name is preserved✓ whitespace normalization works✓ case normalization works✓ legal suffixes are not recklessly removed✓ website normalization exists✓ domain extraction exists✓ invalid websites are rejected✓ domain is server-derived✓ create_company_draft exists✓ update_company_draft exists✓ draft operations do not create Companies✓ missing-field detection is server-owned✓ incomplete draft reports missing name✓ complete draft reports no missing fields✓ field provenance exists✓ user fields are distinguished from derived fields✓ AI does not invent Company details✓ existing Company resolution runs before creation✓ exact normalized-name match resolves correctly✓ duplicate detection is tenant-scoped✓ exact domain match is surfaced✓ cross-tenant Companies are never resolved✓ missing Company can trigger creation workflow✓ Company creation requires explicit user intent✓ Company is not silently created by Opportunity tools✓ prepare_company_creation exists✓ preparation rechecks tenant✓ preparation rechecks user✓ preparation rechecks permission✓ preparation revalidates Company data✓ preparation rechecks duplicates✓ preparation creates no Company✓ company.create MutationRequest exists✓ prepared values exactly match Company draft✓ confirmation expiry works✓ editing draft invalidates old preparation✓ Company creation confirmation widget exists✓ Company name is displayed✓ optional supplied fields are displayed✓ CRM side effect is explicit✓ button says Create Company✓ Cancel creates no Company✓ create_company accepts only mutation_request_id✓ execution rechecks tenant✓ execution rechecks user✓ execution rechecks permission✓ execution rechecks request status✓ execution rechecks expiry✓ execution revalidates draft✓ execution rechecks duplicates✓ Company creation is transactional✓ Company ID is server-generated✓ Company version starts at 1✓ Company AuditEvent is transactional✓ MutationRequest becomes executed✓ CompanyDraft becomes converted✓ failed transaction rolls back everything✓ duplicate execution is idempotent✓ only one Company is created✓ only one AuditEvent is created✓ authoritative Company is returned✓ ChatGPT reports success only after execution✓ Company success widget uses authoritative data✓ OpportunityDraft can retain unresolved Company reference✓ missing Company can be represented as dependency✓ Opportunity data survives Company creation workflow✓ user does not need to re-enter Opportunity details✓ successful Company creation supplies company_id✓ company_id is attached to OpportunityDraft✓ OpportunityDraft is revalidated✓ complete OpportunityDraft can automatically prepare✓ Opportunity still requires separate confirmation✓ Opportunity is not silently created✓ cancelling Company creation does not create Opportunity✓ unresolved OpportunityDraft can remain for later continuation✓ chained Company → Opportunity workflow works✓ Company and Opportunity have separate AuditEvents✓ related operations can share workflow correlation✓ cross-tenant dependency resolution fails safely✓ existing Company skips unnecessary creation workflow✓ exact normalized existing Company is reused✓ original user intent resumes correctly✓ Quorentra reports version 0.7.0
Most importantly:
A user can now describe a business opportunity involving a Company that does not yet exist, allow Quorentra to resolve that dependency conversationally, create the missing Company through an explicit governed mutation, and then continue the original Opportunity workflow without repeating the request.
140. What We Have Achieved
Quorentra can now execute:
"Create a €90,000 Azure migrationopportunity for Adventure Works."
as:
Natural Language ↓Opportunity Intent ↓Resolve Company ↓Company Missing ↓Company Draft ↓Duplicate Check ↓Company Confirmation ↓Create Company ↓Audit Company ↓Resolve Opportunity Dependency ↓Resume Opportunity Draft ↓Opportunity Confirmation ↓Create Opportunity ↓Audit Opportunity ↓Authoritative CRM State
This is no longer simply conversational CRUD.
It is conversational business-process composition.
141. The Architecture Is Starting to Reveal Itself
Our system now has several clearly separated layers:
┌──────────────────────────────────────────┐│ CHATGPT ││ ││ Natural-language interaction ││ Intent extraction ││ Conversational orchestration │└───────────────────┬──────────────────────┘ │ ▼┌──────────────────────────────────────────┐│ QUORENTRA CAPABILITIES ││ ││ Company ││ Opportunity ││ Pipeline ││ Mutation ││ Draft ││ Resolution │└───────────────────┬──────────────────────┘ │ ▼┌──────────────────────────────────────────┐│ APPLICATION / DOMAIN ││ ││ Validation ││ Business Rules ││ Permissions ││ Tenant Isolation ││ Dependency Resolution ││ Transactions │└───────────────────┬──────────────────────┘ │ ▼┌──────────────────────────────────────────┐│ POSTGRESQL ││ ││ Authoritative CRM State ││ Draft State ││ Mutation State ││ Audit State │└──────────────────────────────────────────┘
This separation is exactly what we want.
142. ChatGPT Is Becoming the Operating Layer
Traditional CRM software expects the user to understand the application’s navigation hierarchy.
The user needs to know:
CompaniesContactsOpportunitiesActivitiesTasks
and how those screens relate.
A ChatGPT-native CRM can reverse that relationship.
The user describes:
what they want to accomplish
and ChatGPT determines:
which CRM capabilities are required.
143. But ChatGPT Is Not the System of Record
This remains non-negotiable.
ChatGPT may orchestrate:
Create CompanythenCreate Opportunity
but it cannot bypass:
AuthenticationTenantContextRBACValidationDuplicate RulesConfirmationTransactionsAudit
The conversational interface is powerful precisely because the application underneath remains controlled.
144. A Reusable Dependency Pattern
Part 20 establishes:
Intent ↓Required Entity ↓Resolve ↓Missing? ↓Create Dependency ↓Resume Intent
This pattern will appear repeatedly.
For example:
ContactrequiresCompany
or:
Taskmay referenceContactOpportunityCompany
or:
Meetingmay requireContactsOpportunity
145. The Modular Strategy Is Working
Notice what we did not need to build.
We did not build:
Full Contact ManagementFull Task ManagementWorkflow EngineAI Agent FrameworkRAGVector SearchComplex AutomationEmail IntegrationCalendar Integration
We added one small domain capability:
Company Creation
and connected it to our existing:
Opportunity Creation
That is the modular approach.
146. Each Part Leaves a Running System
After Part 20, Quorentra remains usable.
It does not depend on finishing another fifty architecture chapters before anything works.
We now have a running system capable of:
AuthenticationTenant IsolationCompany ManagementOpportunity ManagementPipeline AnalysisChatGPT ReadsChatGPT MutationsChatGPT Opportunity CreationChatGPT Company CreationMulti-Entity Conversational Workflows
That is already the foundation of a real CRM MVP.
147. The Next Missing CRM Entity
Companies rarely exist without people.
Users need to capture:
Decision MakersTechnical ContactsChampionsEconomic BuyersProcurement ContactsStakeholders
That means our next entity is:
Contact
148. Contact Creation Is More Interesting
Consider:
Add Sarah Johnson at Adventure Works. Her email is sarah.johnson@adventure-works.com.
Quorentra must:
Resolve Adventure Works ↓Detect Contact Duplicate ↓Normalize Email ↓Create Contact Draft ↓Validate ↓Confirm ↓Create Contact ↓Associate Contact with Company ↓Audit
But consider:
Add Sarah Johnson at Blue Yonder Airlines.
If Blue Yonder Airlines does not exist:
Contact Intent ↓Company Missing ↓Create Company ↓Resume Contact ↓Create Contact
The dependency architecture from Part 20 can be reused almost directly.
149. Contact Duplicate Detection
Contacts also introduce stronger identity signals:
Email AddressPhone NumberCompany + Name
We will need to distinguish:
Exact duplicatePotential duplicateDifferent person with same name
This gives us another opportunity to expand entity resolution without jumping prematurely into AI similarity.
150. The Next Article
Creating Contacts from ChatGPT — Conversational Contact Capture, Company Relationships, Duplicate Detection, and Dependency Resolution
We will introduce:
contacts.create permissionContactDraftFirst nameLast nameFull nameEmailPhoneJob titleCompany relationshipEmail normalizationPhone normalizationCompany resolutionCompany dependency handlingContact duplicate detectionExact email matchingCompany + name matchingMissing-field detectionContact validationContact creation confirmationPrepared Contact mutationsIdempotent Contact creationContact AuditEventsContact result widgetCompany → Contact workflowMissing Company → Create Company → Resume ContactContact → Opportunity relationshipsWorkflow correlation
The target interaction will be:
User:"Add Sarah Johnson at Adventure Works.She's the IT Director and her email issarah.johnson@adventure-works.com."
Quorentra will resolve:
Company:Adventure WorksContact:Sarah JohnsonJob Title:IT DirectorEmail:sarah.johnson@adventure-works.com
Then:
Duplicate Check ↓Contact Draft ↓Confirmation ↓Create Contact ↓Audit
And if the Company does not exist:
Contact Intent ↓Company Missing ↓Create Company ↓Resume Contact Draft ↓Confirm Contact ↓Create Contact
That will give Quorentra the three core relationship entities required for the next stage of the MVP:
Company │ ├──── Contact │ └──── Opportunity
From there we can begin connecting the CRM to the work that happens around those relationships:
ActivitiesTasksMeetingsNotes
and eventually allow requests such as:
Add Sarah Johnson at Adventure Works, create a €90,000 Azure migration opportunity, and schedule a follow-up task for Friday.
That is where Quorentra begins evolving from a conversational CRM database into a ChatGPT-native CRM workspace.