Quorentra

Quorentra CRM Creating Companies from ChatGPT: Building from Zero — Part 20

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.

Quorentra Creating Companies from ChatGPT - Building from Zero — Part 20
Quorentra Creating Companies from ChatGPT – Building from Zero — Part 20

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 Company
Detect Missing Company
Propose Company Creation
Collect Missing Information
Detect Duplicates
Normalize Company Data
Validate
Confirm
Create Company
Audit
Resume 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 Works
Opportunity:
Azure Migration
Amount:
€90,000
Expected 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,000
15 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:

Contacts
Opportunities
Activities
Tasks
Meetings
Notes
Documents
Communications
AI 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:

Company
Name
Amount
Currency
Stage
Probability
Expected Close Date

A minimal Company can be much simpler.

For the MVP, we can define:

Required:
name
Optional:
website
industry
phone
country
description

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 Companies
Spelling Variations
Domain Variations
Tenant Leakage
Incorrect Normalization
AI-Invented Details
Repeated 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-ui
npm run build

Verify:

Create a €120,000 Microsoft Copilot Deployment opportunity for Contoso closing November 30, 2026.

Confirm that Quorentra can:

Resolve Contoso
Create Opportunity Draft
Apply Defaults
Validate
Prepare
Confirm
Create Opportunity
Create AuditEvent
Return 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.read
companies.update
companies.delete

Our permission model now begins to look like:

Companies
├── companies.read
├── companies.create
└── companies.update
Opportunities
├── opportunities.read
├── opportunities.create
└── opportunities.update

9. Example Role Matrix

For the MVP:

RoleRead CompanyCreate CompanyUpdate 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:

id
organization_id
name
normalized_name
website
domain
industry
phone
country
description
version
created_at
updated_at

Some fields are user-facing.

Others exist to support reliable application behavior.


11. Classify the Fields

Before creating anything conversationally, classify ownership.

FieldSource
idSystem
organization_idTenantContext
nameUser
normalized_nameServer
websiteUser
domainServer-derived
industryUser
phoneUser
countryUser
descriptionUser
versionSystem
created_atSystem
updated_atSystem

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:

Website
Industry
Country
Phone
Description

The draft architecture gives us room to grow without changing the interaction model.


15. Company Draft Status

Use:

incomplete
complete
prepared
cancelled
expired
converted

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 Works
adventure works
Adventure Works
ADVENTURE WORKS

We should create:

normalized_name

server-side.

A simple MVP normalization can:

trim whitespace
collapse repeated spaces
convert 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:

Ltd
LLC
Inc
B.V.
GmbH
S.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.com
www.adventure-works.com
https://adventure-works.com
https://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 International
adventure-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 name
OR
Exact normalized domain

This remains deterministic.

No embeddings are required.


33. Domain Collision Example

Existing:

Name:
Adventure Works International
Domain:
adventure-works.com

New draft:

Name:
Adventure Works
Domain:
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:

Industry
Country
Phone
Website
Employee count
Revenue

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_id
normalized_name
domain
version
created_at
updated_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:

Normalization
Validation
Duplicate Recheck
Missing-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_NAME
EXACT_DOMAIN

Later:

SIMILAR_NAME

may be added.

For the MVP, deterministic exact signals are sufficient.


48. Do Not Use AI Similarity Yet

Avoid introducing:

Embeddings
Semantic search
LLM duplicate classification

into Company creation now.

Our goal is a running modular MVP.

Start with:

Normalized Name
Domain

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_name
domain
organization_id
version

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:

Company
Adventure Works
Website
adventure-works.com
Industry
Manufacturing
Country
Netherlands

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 Company
Subsidiary
Business Unit

sharing a domain.

Instead, treat exact domain match as a strong warning.


66. Transaction Boundary

Company creation should execute inside:

BEGIN
create Company
create AuditEvent
mark MutationRequest executed
mark CompanyDraft converted
COMMIT

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_id
actor_user_id
entity_id
new_values
invocation_source
mutation_request_id
created_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 Migration
Amount:
€90,000
Expected 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 = NULL
company_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:

resolved
unresolved
waiting_for_creation
failed

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 Company
then
Confirm Opportunity

This preserves the simple mutation architecture we already trust.


79. Why Separate Confirmations?

Because they are two authoritative business effects:

Create Company
Create 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 Company
Create Primary Contact
Create Opportunity
Create Follow-Up Task

with one confirmation.

But that would require:

Cross-entity transaction semantics
Partial failure policy
Workflow-level idempotency
Workflow-level audit
Compensation 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:

Validation
Missing-field detection
Duplicate awareness

84. Complete Opportunity Draft

Our original draft already had:

name
amount
currency
expected_close_date

and defaults supplied:

stage
probability

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 Company
Adventure Works

User confirms.

Quorentra:

Adventure Works created.

Then immediately:

Create Opportunity
Azure Migration
Adventure Works
€90,000
Qualification
20%
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 Companies
Click New Company
Enter Name
Save
Return to Opportunities
Click New Opportunity
Search Company
Enter Amount
Select Stage
Select Date
Save

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 Isolation
RBAC
Entity Resolution
Draft State
Normalization
Validation
Duplicate Detection
Confirmation
Idempotency
Transactions
Audit
Dependency Resolution
Continuation

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 created
Opportunity 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 User
Company

This makes the workflow robust under concurrency.


95. Audit Both Operations Separately

The chained workflow produces:

AuditEvent 1
company.created
AuditEvent 2
opportunity.created

Each retains its own:

actor
timestamp
entity
mutation_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:

OpportunityDraft
CompanyDraft
MutationRequest
AuditEvent

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_model
chatgpt_widget
rest
system

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_id
opportunity_draft_id
company_draft_id
company_mutation_request_id
opportunity_mutation_request_id
organization_id
user_id
current_step
result
duration_ms

Do not log unnecessary business content.


101. Company Draft Tests

Test:

create Company draft
update Company draft
expire Company draft
cancel Company draft
tenant isolation
user 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.com
www.adventure-works.com
https://adventure-works.com
https://www.adventure-works.com/

according to the normalization rules you define.

Verify:

domain =
adventure-works.com

where appropriate.


104. Duplicate Tests

Test:

exact normalized name
exact domain
different tenant same name
different tenant same domain
similar but non-exact name

Ensure only current-tenant records influence the result.


105. Permission Tests

Viewer:

create_company_draft → denied
prepare_company_creation → denied
create_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 prepare
expired draft cannot prepare
duplicate policy enforced
valid complete draft can prepare
preparation 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 Company
use active tenant
use confirmed values
generate Company ID
set version = 1
create AuditEvent
execute MutationRequest
convert CompanyDraft
return authoritative Company

110. Idempotency Test

Execute the same Company MutationRequest twice.

Expected:

1 Company
1 AuditEvent
1 logical result

Never:

2 Companies

111. Transaction Failure Test

Simulate failure while creating the AuditEvent.

Expected:

Company creation rolled back
MutationRequest remains unexecuted
CompanyDraft 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 created
Company dependency unresolved
Company creation offered

114. Chained Workflow Cancellation Test

Cancel Company creation.

Expected:

No Company
No Opportunity
Opportunity Draft remains unresolved

115. Chained Workflow Success Test

Confirm Company.

Expected:

Company created
Company AuditEvent created
OpportunityDraft.company_id populated
Opportunity Draft revalidated
Opportunity preparation available

Then confirm Opportunity.

Expected:

Opportunity created
Opportunity AuditEvent created
Original 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 required
Company missing

and suggest the next action.

That is orchestration.

The actual creation remains:

Explicit
Confirmed
Audited

122. ChatGPT as Conversational Orchestrator

ChatGPT’s role is expanding.

It can now coordinate:

User Intent
Company Resolution
Company Creation
Opportunity Continuation
Opportunity Creation

But it still does not own:

Permissions
Tenant Scope
Business Rules
Transactions
Authoritative 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:

OpportunityDraft
CompanyDraft
MutationRequest
workflow_id
dependency 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 Works
Detect Sarah Johnson duplicates
Collect email/phone if available
Create Contact Draft
Confirm
Create Contact
Link Contact to Company
Audit

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 Module
Contact Module
Opportunity Module
Task Module
Activity 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_company
create_company_draft
prepare_company_creation
create_company
create_opportunity_draft
prepare_opportunity_creation
create_opportunity

This keeps the architecture inspectable.


131. AI Composition, Deterministic Execution

A useful summary is:

AI
Composition
Application
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 name
Prefix/name search
Domain 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:

query
limit

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 creation
Company normalization
Duplicate prevention
Dependency-aware continuation
Chained 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 migration
opportunity 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:

Companies
Contacts
Opportunities
Activities
Tasks

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 Company
then
Create Opportunity

but it cannot bypass:

Authentication
TenantContext
RBAC
Validation
Duplicate Rules
Confirmation
Transactions
Audit

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:

Contact
requires
Company

or:

Task
may reference
Contact
Opportunity
Company

or:

Meeting
may require
Contacts
Opportunity

145. The Modular Strategy Is Working

Notice what we did not need to build.

We did not build:

Full Contact Management
Full Task Management
Workflow Engine
AI Agent Framework
RAG
Vector Search
Complex Automation
Email Integration
Calendar 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:

Authentication
Tenant Isolation
Company Management
Opportunity Management
Pipeline Analysis
ChatGPT Reads
ChatGPT Mutations
ChatGPT Opportunity Creation
ChatGPT Company Creation
Multi-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 Makers
Technical Contacts
Champions
Economic Buyers
Procurement Contacts
Stakeholders

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 Address
Phone Number
Company + Name

We will need to distinguish:

Exact duplicate
Potential duplicate
Different person with same name

This gives us another opportunity to expand entity resolution without jumping prematurely into AI similarity.


150. The Next Article

In Part 21, we will build:

Creating Contacts from ChatGPT — Conversational Contact Capture, Company Relationships, Duplicate Detection, and Dependency Resolution

We will introduce:

contacts.create permission
ContactDraft
First name
Last name
Full name
Email
Phone
Job title
Company relationship
Email normalization
Phone normalization
Company resolution
Company dependency handling
Contact duplicate detection
Exact email matching
Company + name matching
Missing-field detection
Contact validation
Contact creation confirmation
Prepared Contact mutations
Idempotent Contact creation
Contact AuditEvents
Contact result widget
Company → Contact workflow
Missing Company → Create Company → Resume Contact
Contact → Opportunity relationships
Workflow correlation

The target interaction will be:

User:
"Add Sarah Johnson at Adventure Works.
She's the IT Director and her email is
sarah.johnson@adventure-works.com."

Quorentra will resolve:

Company:
Adventure Works
Contact:
Sarah Johnson
Job Title:
IT Director
Email:
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:

Activities
Tasks
Meetings
Notes

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.

Quorentra

Contact

Menu

Designed with WordPress

Discover more from Quorentra

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

Continue reading