AI CRM / Sales Workflow Engine
What this solves
Sales and customer data was coming in from multiple places — forms, emails, transcripts, chat, product events, and HubSpot webhooks — but the CRM was always behind. Reps were wasting time cleaning records, writing summaries, and deciding who to follow up with. The goal was to build a backend workflow engine that ingested those events, enriched them with AI, and wrote useful outputs back into HubSpot automatically.
Stack
GoAWSPostgreSQLRedisSQSorKafkaHubSpot APIOpenAIorAnthropicECS FargateCloudWatch
High-level architecture
flowchart LR
SRC[Forms / Emails / Transcripts / Product Events / HubSpot Webhooks] --> API[Go API]
API --> PG[(PostgreSQL)]
API --> REDIS[Redis]
API --> Q[Queue]
Q --> NORM[Normaliser Worker]
NORM --> PG
NORM --> Q
Q --> AI[AI Enrichment Worker]
AI --> LLM[LLM Provider]
AI --> PG
AI --> Q
Q --> CRM[HubSpot Writeback Worker]
CRM --> HUB[HubSpot API]
CRM --> PG
ADMIN[Admin UI] --> API
How it worked
- An event arrived from a form, transcript, email, product action, or HubSpot webhook
- The backend stored the raw event first
- A worker normalised it into a common internal format
- An AI worker generated:
- summary
- intent / lead quality
- next action
- follow-up draft
- A writeback worker updated HubSpot with notes, tasks, or property changes
- Everything was logged for replay, audit, and debugging
Main flow
flowchart TD
A[Incoming Event] --> B[Store Raw Payload]
B --> C[Deduplicate]
C --> D[Normalise Event]
D --> E[AI Enrichment]
E --> F[Generate Summary / Score / Follow-up]
F --> G[Write Back to HubSpot]
G --> H[Store Audit + Result]
Core services
api-service
- receives inbound events
- authenticates internal users
- exposes admin and replay endpoints
normaliser-worker
- maps raw payloads into a stable internal model
- links event to contact, company, or deal
enrichment-worker
- builds prompts
- calls the LLM
- stores summaries, scores, and follow-up drafts
hubspot-writeback-worker
- creates notes, tasks, and property updates in HubSpot
- handles retries and idempotency
Data model
| Table | Purpose |
|---|---|
raw_events |
Original payloads |
normalised_events |
Clean internal event model |
crm_entities |
Contact / company / deal mappings |
enrichment_outputs |
AI summary, score, next step |
crm_actions |
Planned HubSpot updates |
crm_action_results |
Writeback success/failure |
audit_logs |
Replay and operator history |
Key decisions
- raw events were always stored first
- AI never wrote directly to HubSpot
- every writeback was idempotent
- low-confidence or broken events could be reviewed manually
- prompt inputs included CRM context, not just raw text
Example output
For a sales call transcript, the system might generate:
- short summary
- buyer intent:
high - objections:
security,budget - recommended next action
- follow-up email draft
- lead score change
Small code example
func (w *EnrichmentWorker) Handle(ctx context.Context, eventID string) error {
event, err := w.Events.GetByID(ctx, eventID)
if err != nil {
return err
}
prompt := w.Prompts.BuildSalesPrompt(event)
result, err := w.LLM.Generate(ctx, prompt)
if err != nil {
return err
}
if err := w.Enrichments.Store(ctx, eventID, result.Text); err != nil {
return err
}
return w.Queue.PublishCRMWriteback(ctx, eventID)
}