Muhammad Dhiyaul AthaMatt runs a growing business, but his daily intake operation was becoming a bottleneck. His team...
Matt runs a growing business, but his daily intake operation was becoming a bottleneck.
His team receives business inquiries through email, website forms, and messaging channels. The incoming information is highly inconsistent. Some inquiries are valuable sales opportunities, some are support questions, some are junk, and others simply lack enough information to make a decision.
He needed a system to ingest these inquiries, identify what they are, extract useful structured information, and draft the next response. However, sending sensitive client business data to closed-source cloud LLMs was a major privacy concern.
For the Hacktoberfest Weekend Challenge: Build for a Friend, I decided to build him a secure, locally hosted AI CRM Intake Automation System written in Go.
I designed a practical backend system that ingests incoming messages, uses local open-source AI to classify them, extracts structured data, and prepares the next steps for a human to review.
Here is what the system does:
Sales Lead, Support Ticket, Junk, or Incomplete.The prototype can process an inquiry locally and produce structured output for the next stage of the workflow.
You can check out the project and its code structure here:
GitHub: CRM Automation System
I built the workflow engine in Go around a simple principle:
The AI analyzes the data, but deterministic code executes the actions.
The LLM is responsible for classification, extraction, and drafting. It does not directly send emails, modify CRM records, or execute arbitrary actions. The deterministic Go backend handles validation, duplicate protection, idempotency, database updates, and the audit trail.
Consequential actions require human approval. Before an email is sent or a CRM record is updated, the workflow pauses and creates a pending approval for the operator to review.
// internal/workflow/approval_service.go
package workflow
import (
"context"
"fmt"
"time"
)
func (s *ActionService) ApproveAction(
ctx context.Context,
actionID, approverID string,
) (ActionRecord, error) {
action, err := s.repo.Get(ctx, actionID)
if err != nil {
return ActionRecord{}, err
}
if action.State != ActionStatePendingApproval {
return ActionRecord{}, fmt.Errorf(
"invalid approval transition from %s",
action.State,
)
}
updated := action
updated.State = ActionStateApproved
updated.ApprovedBy = approverID
updated.UpdatedAt = time.Now()
if err := s.repo.Update(ctx, updated); err != nil {
return ActionRecord{}, err
}
if err := s.audit.Append(
ctx,
createAuditEvent(
action.InquiryID,
action.ID,
"approval.granted",
DecisionAllow,
"approval",
),
); err != nil {
return ActionRecord{}, err
}
return updated, nil
}
For this project, using open-source AI locally through Ollama wasn't just a technical choice; it was a business requirement.
Business inquiries can contain sensitive information such as budgets and personal contact details. By keeping AI inference local, the system does not need to send that data to a third-party LLM provider.
Running an open-weight model locally avoids per-token API costs for every inquiry, including messages that eventually turn out to be spam or incomplete.
Running the model locally gives me more control over the model, prompting, and output format. The Go backend validates the structured JSON response before processing it.
This project reinforced my interest in building AI systems where models are useful for analysis and drafting, while deterministic software and human approval remain responsible for consequential actions.
Happy hacking! 🌟