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.
What I Built
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:
-
Ingestion & Classification: Determines if a message is a
Sales Lead,Support Ticket,Junk, orIncomplete. - Extraction: Pulls out names, contact info, and business needs.
- Drafting: Prepares a response based on the classification.
- Human-in-the-Loop: Alerts Matt's team to review and approve the action before any CRM record is updated or any email is sent.
Demo
The prototype can process an inquiry locally and produce structured output for the next stage of the workflow.
Code
You can check out the project and its code structure here:
GitHub: CRM Automation System
How I Built It
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.
Human-in-the-Loop
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
}
Why Does Open Innovation Matter?
For this project, using open-source AI locally through Ollama wasn't just a technical choice; it was a business requirement.
Data Privacy
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.
Cost Control
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.
Control and Predictability
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.
Conclusion
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! 🌟

Top comments (0)