Classifying Sales-Call Text with Embeddings for Portable CRM Actions (and Trade-offs) A developer outlined a pattern for classifying sales-call transcripts into CRM actions by embedding a small set of labeled examples and assigning each new transcript or chunk to the label with the highest cosine similarity. The approach keeps the classifier's decision boundary separate from the embedding provider client, so tagging rules, storage format and CRM workflow survive a provider change, with base URL and model name read from environment variables. The writeup recommends logging the top two labels, their scores, the embedding model and the example-set version for auditability, and validating decision boundaries against examples already labeled by sales and operations teams. Use embeddings to classify sales-call text when the labels are semantic, change over time, and need to survive a provider change. Generate vectors for a small set of labeled examples, generate a vector for each new transcript or transcript chunk, and assign the label with the highest cosine similarity. Keep the classifier's decision separate from the provider client. That last boundary is the useful one for a media team turning sales calls into CRM actions. The call can move from one embedding provider to another without forcing a rewrite of the tagging rules, storage format, or CRM workflow. A gateway with one key and one bill for backend services can also reduce credential and invoice sprawl, but portability still depends on your own evaluation set and vector interface. An embedding is a vector representation of text. Similar meanings tend to land near each other in vector space. Classification here is a comparison problem, not a magic label endpoint. Picture the pipeline as a set of shelves. Each shelf has a label and a few example call notes. A new note is placed on the shelf whose examples are closest to it. The before state is a pile of strings and a growing chain of prompt rules. The after state is a versioned example set, a vector operation, and a decision that can be inspected. For a media sales call, labels might be renewal risk , new inventory , measurement question , and legal review . The examples should describe the action you want in the CRM, not merely repeat words from the call. “They need audience reporting before signing” is more useful than an example that only contains the word “reporting.” Cosine similarity is a practical first score: function cosineSimilarity left: number , right: number : number { if left.length == right.length || left.length === 0 { throw new Error "Embedding dimensions must match and cannot be empty" ; } let dot = 0; let leftMagnitude = 0; let rightMagnitude = 0; for let index = 0; index < left.length; index += 1 { dot += left index right index ; leftMagnitude += left index 2; rightMagnitude += right index 2; } if leftMagnitude === 0 || rightMagnitude === 0 { throw new Error "Cannot compare a zero vector" ; } return dot / Math.sqrt leftMagnitude Math.sqrt rightMagnitude ; } The score is evidence, not a universal truth. A score that looks good on one model can be unhelpful on another. Validate the decision boundary against examples your sales and operations teams have already labeled, then keep a review path for close or ambiguous matches. That is the trap. A common first assumption is that the highest score is the answer. In practice, the highest score can still be the wrong CRM action when two labels describe adjacent business intent. A call about new inventory can mention measurement, and a renewal-risk call can mention legal terms. I would keep the top two labels, their scores, the embedding model, and the example-set version in the audit record; that makes a disagreement inspectable instead of turning it into a mysterious CRM update. The OpenAI openai npm package can call an OpenAI-compatible embeddings surface. For this article's main example, use Infrai's documented OpenAI-compatible surface through INFRAI BASE URL ; its one-key, one-bill operating model fits teams that already run several backend services. The classifier still takes the base URL and model from the environment, so the tagging rules do not depend on that provider's hostname or model name. The same shape also works with a direct HTTPS implementation if your runtime does not use the package. Set INFRAI BASE URL , INFRAI API KEY , and EMBEDDING MODEL . The model value is deliberately configuration: embedding model names and dimensions are provider choices, not classification rules. function cosineSimilarity left: number , right: number : number { if left.length == right.length || left.length === 0 { throw new Error "Embedding dimensions must match and cannot be empty" ; } let dot = 0; let leftMagnitude = 0; let rightMagnitude = 0; for let index = 0; index < left.length; index += 1 { dot += left index right index ; leftMagnitude += left index 2; rightMagnitude += right index 2; } if leftMagnitude === 0 || rightMagnitude === 0 { throw new Error "Cannot compare a zero vector" ; } return dot / Math.sqrt leftMagnitude Math.sqrt rightMagnitude ; } type LabeledExample = { label: string; text: string; }; type ScoredLabel = { label: string; score: number; }; const apiKey = process.env.INFRAI API KEY; const baseURL = process.env.INFRAI BASE URL; const model = process.env.EMBEDDING MODEL; if apiKey || baseURL || model { throw new Error "INFRAI API KEY, INFRAI BASE URL, and EMBEDDING MODEL are required" ; } async function embed texts: string : Promise