10 esempi pratici
25Esempio 7 · Vendite: qualificare i lead
Un flow legge un elenco di potenziali clienti, fa dare a un agente un punteggio strutturato con la motivazione e, solo per quelli caldi, prepara una bozza di email che approvi tu. Nessuna email parte davvero.
Una piccola azienda di software riceve ogni settimana decine di contatti: richieste dal modulo del sito, biglietti da visita raccolti in fiera, iscritti a un webinar. Il commerciale è uno solo. Se risponde a tutti nello stesso ordine, il cliente pronto a comprare aspetta dietro allo studente che scrive la tesi.
Nel gergo delle vendite questi contatti si chiamano Lead: Nel marketing e nelle vendite, un potenziale cliente di cui si hanno almeno i dati di contatto e un segnale di interesse. glossario. Il lavoro da automatizzare ha tre passi: dare a ogni lead un punteggio con una motivazione, smistarli, e scrivere una prima email solo a quelli più promettenti. In questo esempio lo fai con un Flow: Il "copione" che orchestra passi, crew e agenti con uno stato condiviso, condizioni e diramazioni. La documentazione lo consiglia come struttura per le applicazioni vere. glossario. Un agente valuta, il codice decide la strada, un secondo agente scrive la bozza e tu la approvi prima che vada da qualche parte.
- Problema
- Mettere in ordine di priorità un elenco di lead e preparare email personalizzate solo per quelli caldi, senza mandare niente in automatico.
- La squadra (agenti)
- Analista commerciale (dà il punteggio), Autore di email (scrive la bozza). Il flow fa da capo reparto.
- Strumenti
- Nessuno strumento per gli agenti: i dati arrivano da un file CSV letto da Python.
- Chiavi e costi
- Solo il modello (
OPENAI_API_KEY, qualche centesimo per cinque lead) oppure gratis con Ollama. I lead freddi non costano nulla: per loro non si chiama il modello. - Cosa impari di nuovo
- Un router che decide con soglie scritte nel codice su un punteggio ottenuto con
agente.kickoff(..., response_format=...); un flow nuovo per ogni riga di un CSV;human_input=Trueper approvare una bozza nel terminale; minimizzare i dati personali mandati al modello.
Perché è tra i più usati#
Nella classifica vendite e qualificazione dei lead sono al 7° posto, con 33,6 punti su 100. È un caso particolare: poco visibile su GitHub (solo 91 repository con «crewai sales» nel nome o nella descrizione, 103 con «lead»), ma tra i più presenti nel materiale di CrewAI e nelle aziende.
- Tra le storie dei clienti pubblicate da CrewAI: DocuSign dichiara un primo contatto con i lead più veloce del 75%, Gelato oltre 3.000 lead arricchiti al mese, Brickell Digital +80% di lead qualificati. Sono numeri comunicati dal fornitore, non verificati in modo indipendente.
- Nel sondaggio PwC del 2025 su 308 dirigenti statunitensi, il 54% usa o prevede di usare agenti in vendite e marketing. Il repository ufficiale ha un flow dedicato, lead-score-flow, con valutazione e revisione umana.
Perché così pochi progetti pubblici? Probabilmente perché una crew di vendita lavora sui clienti veri di un'azienda e vive in repository privati. È una delle ragioni per cui la classifica non si basa solo su GitHub.
Lo schema#
Rispetto agli esempi precedenti la struttura cambia. Non c'è una sola crew dall'inizio alla fine, ma un flow con Stato (di un flow): I dati che un flow porta con sé da un passo all'altro, per esempio l'argomento scelto e la bozza scritta. Si leggono e scrivono con self.state. glossario (capitolo 16) che decide quando serve un agente e quando no. Il Router: Un passo di un flow che sceglie la strada successiva restituendo un'etichetta, per esempio "approvato" o "da_rivedere". glossario non chiede al modello «è caldo?»: legge il punteggio e applica le soglie. Così le regole sono uguali per tutti i lead e le puoi cambiare in un punto solo.
Preparazione#
Crea una cartella vendite-lead e aprila nel terminale. Il file .env:
MODEL=openai/gpt-4.1-mini
OPENAI_API_KEY=sk-la-tua-chiave
CREWAI_DISABLE_TELEMETRY=trueCon Ollama scrivi MODEL=ollama/qwen2.5:7b e togli la chiave. Per dati di clienti veri è anche la scelta più prudente, come vedrai nella sezione sulla privacy.
Poi scarica lead_fittizi.csv nella stessa cartella. I cinque lead sono inventati: persone e aziende non esistono, e gli indirizzi usano example.com, un dominio riservato apposta per gli esempi che non recapita posta a nessuno. Il prodotto da vendere, «Magazzino Chiaro», è inventato anche lui.
nome,azienda,ruolo,settore,dipendenti,fonte,messaggio,email
Giulia Ferri,Ortofrutta Ferri,titolare,commercio alimentare,12,modulo sul sito,"Abbiamo tre punti vendita e gestiamo il magazzino su Excel: vorrei una demo entro fine mese.",giulia.ferri@example.com
Marco Bellini,Studio Bellini,praticante,consulenza,3,newsletter,"Mi sono iscritto per curiosità, al momento non ci serve nulla.",marco.bellini@example.com
Elena Riva,Riva Ricambi Auto,responsabile acquisti,ricambi auto,45,fiera,"Ci siamo parlati in fiera: chiedo il listino per 5 utenti e se vi collegate al nostro negozio online.",elena.riva@example.com
Paolo Conti,,studente,,,modulo sul sito,"Sto scrivendo la tesi sui software gestionali, posso farvi qualche domanda?",paolo.conti@example.com
Sara Galli,Galli Arredamenti,amministrazione,arredamento,20,webinar,"Webinar interessante. Valuteremo il cambio di gestionale l'anno prossimo.",sara.galli@example.comPrima di lanciare lo script, prova a fare tu la classifica: chi chiameresti per primo? Ti servirà per giudicare il lavoro dell'agente.
Il codice, pezzo per pezzo#
Il punteggio e lo stato#
class Valutazione(BaseModel):
punteggio: int = Field(ge=0, le=100, description="Da 0 (nessun interesse) a 100 (pronto ad acquistare)")
segnali: list[str] = Field(description="I fatti del lead che giustificano il punteggio")
motivazione: str = Field(description="Spiegazione in una o due frasi, in italiano")
class StatoLead(BaseModel):
lead: dict[str, str] = {}
valutazione: Valutazione | None = None
esito: str = ""Valutazione è l'Output strutturato: Un risultato con campi fissi (titolo, lista, numero…) invece di testo libero, così un programma può usarlo senza interpretarlo. In CrewAI: output_pydantic. glossario dell'agente. Field(ge=0, le=100) significa «maggiore o uguale a 0, minore o uguale a 100»: un punteggio di 150 viene rifiutato da Pydantic: Libreria Python per descrivere la forma dei dati (campi e tipi) e controllarla. CrewAI la usa per ottenere risposte strutturate e verificate. glossario. I segnali obbligano il modello a dire su quali fatti si basa, e ti permettono di accorgerti se ne cita uno che nel lead non c'è.
StatoLead è lo stato del flow: il lead di partenza, la valutazione, l'esito finale. Valutazione | None = None vuol dire «all'inizio non c'è ancora».
La valutazione: un agente, senza crew#
@start()
def valuta(self):
# Minimizzazione dei dati: al modello non serve l'indirizzo email.
dati = {chiave: valore for chiave, valore in self.state.lead.items() if chiave != "email"}
risposta = valutatore.kickoff(
f"La nostra offerta: {OFFERTA}\nIl lead: {dati}\n"
"Valuta quanto questo lead è pronto a diventare cliente. Rispondi in italiano.",
response_format=Valutazione,
)
self.state.valutazione = risposta.pydanticCome nell'esempio 3, invece di creare un task e una crew per un solo agente chiamiamo direttamente valutatore.kickoff(...): un agente può lavorare anche da solo, ricevendo il testo della richiesta. È lo schema che la skill ufficiale design-agent consiglia dentro i flow: il flow tiene l'ordine dei passi e lo stato, ogni passo è il lavoro di un agente. response_format=Valutazione fa per l'agente quello che output_pydantic fa per un task, e il risultato si legge da risposta.pydantic.
La prima riga toglie l'email dal lead prima di passarlo al modello. Per dare un punteggio l'indirizzo non serve, quindi non lo mandiamo. È un piccolo esempio di un principio del GDPR: Il Regolamento generale sulla protezione dei dati (Regolamento UE 2016/679): le regole europee su come si raccolgono, usano e conservano i dati personali. glossario: usare solo i dati necessari.
Il router: decide il codice#
@router(valuta)
def smista(self):
# La soglia la decide il codice, non il modello: stesse regole per tutti i lead.
punteggio = self.state.valutazione.punteggio
if punteggio >= 70:
return "caldo"
if punteggio >= 40:
return "tiepido"
return "freddo"
@listen("tiepido")
def da_ricontattare(self):
self.state.esito = "tiepido: da ricontattare più avanti, nessuna bozza"
@listen("freddo")
def archivia(self):
self.state.esito = "freddo: archiviato"Il router restituisce un'etichetta e il flow esegue il metodo in ascolto su quell'etichetta. I rami "tiepido" e "freddo" non chiamano nessun modello: scrivono l'esito e basta. Le soglie 70 e 40 sono una scelta nostra per l'esempio. In un'azienda vera si decidono guardando i lead del passato: quali punteggi avevano quelli che poi sono diventati clienti?
La bozza, con la tua approvazione#
@listen("caldo")
def prepara_bozza(self):
nome_file = "bozze/" + self.state.lead["nome"].lower().replace(" ", "-") + ".md"
email = Task(
description=(
"Scrivi la prima email a {nome}, {ruolo} di {azienda}, che ci ha scritto: «{messaggio}».\n"
"La nostra offerta: {offerta}\nPerché è un lead caldo: {motivazione}\n"
"Rispondi alla sua richiesta concreta e proponi un solo passo successivo. "
"Non inventare prezzi, sconti o funzioni."
),
expected_output="Oggetto e testo dell'email in italiano, massimo 120 parole, firmata 'Il team di Magazzino Chiaro'.",
agent=autore_email,
human_input=True, # prima di chiudere il task, la bozza aspetta il tuo parere nel terminale
output_file=nome_file,
)
Crew(agents=[autore_email], tasks=[email]).kickoff(
inputs={**self.state.lead, "offerta": OFFERTA, "motivazione": self.state.valutazione.motivazione}
)
self.state.esito = f"caldo: bozza approvata e salvata in {nome_file} (nessuna email inviata)"Per la bozza serve un task vero, perché human_input è un parametro dei task. Con human_input=True, quando l'agente ha finito CrewAI mostra la bozza nel terminale e un riquadro giallo «Human Feedback Required» con le istruzioni, in inglese. Se ti va bene premi Invio senza scrivere niente. Altrimenti scrivi cosa cambiare («più breve», «dai del lei») e premi Invio: l'agente riscrive e te la ripropone, finché non premi Invio a vuoto. È lo Human in the loop: Un punto del processo in cui una persona controlla o approva prima che il lavoro prosegua. In CrewAI: human_input=True o @human_feedback. glossario del capitolo 16.
{**self.state.lead, ...} unisce in un solo dizionario i campi del lead (nome, ruolo, azienda, messaggio…) e le due informazioni in più, così tutti i Segnaposto {…}: Una parola tra graffe, come {citta}, dentro i testi di agenti e task. Al kickoff(inputs={...}) viene sostituita con il valore vero. glossario della descrizione trovano il loro valore. La bozza approvata finisce in bozze/giulia-ferri.md o simile. Nessuna email viene inviata: lo script non ha nessuno strumento per farlo, ed è voluto.
Infine, in fondo allo script, un ciclo crea un flow nuovo per ogni lead. Così ognuno parte con uno stato pulito e i dati di un cliente non finiscono per sbaglio nella valutazione del successivo.
Il codice completo#
# /// script
# requires-python = ">=3.10,<3.14"
# dependencies = ["crewai[tools]==1.15.21"]
# ///
"""Un flow valuta i lead di un CSV e, solo per quelli caldi, prepara una bozza di email da approvare."""
import csv
import os
import sys
from pathlib import Path
from crewai import Agent, Crew, Task
from crewai.flow.flow import Flow, listen, router, start
from pydantic import BaseModel, Field
# Il modello si sceglie nel file .env (capitolo 7). Se MODEL manca, si usa quello scritto qui.
MODELLO = os.getenv("MODEL", "openai/gpt-4.1-mini")
FILE_LEAD = "lead_fittizi.csv"
OFFERTA = (
"Magazzino Chiaro (prodotto inventato per l'esempio): software per gestire il magazzino "
"di piccoli negozi e aziende, con prova gratuita di 30 giorni."
)
# Il risultato strutturato della valutazione di un lead.
class Valutazione(BaseModel):
punteggio: int = Field(ge=0, le=100, description="Da 0 (nessun interesse) a 100 (pronto ad acquistare)")
segnali: list[str] = Field(description="I fatti del lead che giustificano il punteggio")
motivazione: str = Field(description="Spiegazione in una o due frasi, in italiano")
# Lo stato che il flow porta da un passo all'altro.
class StatoLead(BaseModel):
lead: dict[str, str] = {}
valutazione: Valutazione | None = None
esito: str = ""
valutatore = Agent(
role="Analista commerciale che qualifica i lead",
goal="Dare a ogni lead un punteggio onesto da 0 a 100, basato solo sui dati ricevuti",
backstory=(
"Lavori da anni con chi vende software alle piccole imprese. Sai che un punteggio gonfiato "
"fa perdere tempo a tutti, quindi guardi segnali concreti: bisogno espresso, urgenza, "
"ruolo di chi scrive, dimensione dell'azienda."
),
llm=MODELLO,
)
autore_email = Agent(
role="Autore di email commerciali personalizzate",
goal="Scrivere una prima email breve, cordiale e specifica, senza promesse esagerate",
backstory=(
"Scrivi email che si leggono fino in fondo: poche righe, un riferimento preciso a ciò che "
"la persona ha chiesto, una sola proposta chiara."
),
llm=MODELLO,
verbose=True,
)
class QualificaLead(Flow[StatoLead]):
@start()
def valuta(self):
# Minimizzazione dei dati: al modello non serve l'indirizzo email.
dati = {chiave: valore for chiave, valore in self.state.lead.items() if chiave != "email"}
risposta = valutatore.kickoff(
f"La nostra offerta: {OFFERTA}\nIl lead: {dati}\n"
"Valuta quanto questo lead è pronto a diventare cliente. Rispondi in italiano.",
response_format=Valutazione,
)
self.state.valutazione = risposta.pydantic
@router(valuta)
def smista(self):
# La soglia la decide il codice, non il modello: stesse regole per tutti i lead.
punteggio = self.state.valutazione.punteggio
if punteggio >= 70:
return "caldo"
if punteggio >= 40:
return "tiepido"
return "freddo"
@listen("caldo")
def prepara_bozza(self):
nome_file = "bozze/" + self.state.lead["nome"].lower().replace(" ", "-") + ".md"
email = Task(
description=(
"Scrivi la prima email a {nome}, {ruolo} di {azienda}, che ci ha scritto: «{messaggio}».\n"
"La nostra offerta: {offerta}\nPerché è un lead caldo: {motivazione}\n"
"Rispondi alla sua richiesta concreta e proponi un solo passo successivo. "
"Non inventare prezzi, sconti o funzioni."
),
expected_output="Oggetto e testo dell'email in italiano, massimo 120 parole, firmata 'Il team di Magazzino Chiaro'.",
agent=autore_email,
human_input=True, # prima di chiudere il task, la bozza aspetta il tuo parere nel terminale
output_file=nome_file,
)
Crew(agents=[autore_email], tasks=[email]).kickoff(
inputs={**self.state.lead, "offerta": OFFERTA, "motivazione": self.state.valutazione.motivazione}
)
self.state.esito = f"caldo: bozza approvata e salvata in {nome_file} (nessuna email inviata)"
@listen("tiepido")
def da_ricontattare(self):
self.state.esito = "tiepido: da ricontattare più avanti, nessuna bozza"
@listen("freddo")
def archivia(self):
self.state.esito = "freddo: archiviato"
if __name__ == "__main__":
if not Path(FILE_LEAD).exists():
sys.exit(f"Non trovo {FILE_LEAD}: scaricalo e mettilo nella stessa cartella dello script.")
with open(FILE_LEAD, encoding="utf-8", newline="") as f:
tutti_i_lead = list(csv.DictReader(f))
riepilogo = []
for lead in tutti_i_lead:
flow = QualificaLead() # un flow nuovo, con uno stato pulito, per ogni lead
flow.kickoff(inputs={"lead": lead})
v = flow.state.valutazione
riepilogo.append(f"{lead['nome']}: {v.punteggio}/100, {flow.state.esito}\n perché: {v.motivazione}")
print("\n=== RIEPILOGO DEI LEAD ===")
print("\n".join(riepilogo))Eseguirlo#
Dalla cartella vendite-lead:
uv run --env-file .env qualifica_lead.pyQuesta volta resta davanti al terminale: lo script si ferma e aspetta te a ogni lead caldo. Vedrai i riquadri del flow (i passi valuta, smista e il ramo scelto), poi per ogni lead caldo la bozza dell'autore e la richiesta di feedback. Alla fine compare il riepilogo con punteggio, esito e motivazione di ciascuno. Le bozze approvate sono nella cartella bozze.
Non abbiamo eseguito questo esempio dal vivo con un modello, quindi qui non trovi un output reale. Lo script è stato verificato con CrewAI 1.15.21 senza chiamare il modello: il flow è stato fatto girare con punteggi finti (85, 72, 50, 10 e 5) e ha smistato i lead come previsto, con due bozze sui lead caldi, un tiepido e due freddi, e senza mai passare l'email all'agente.
Privacy e GDPR, in breve#
Qui i dati non sono numeri di un'azienda inventata: in un caso reale sono persone. Nome, ruolo, azienda e messaggio di un lead sono dati personali e ricadono nel GDPR, il regolamento europeo sulla protezione dei dati. Non è una consulenza legale, ma ci sono alcuni punti da conoscere prima di usare questo flow sul serio.
- Serve una ragione valida per usare quei dati (la «base giuridica», articolo 6), e le persone devono sapere come li tratti. Chi ti ha scritto chiedendo una demo si aspetta una risposta. Chi ha lasciato il biglietto in fiera per un'altra ragione, forse no.
- Manda al modello solo quello che serve (minimizzazione, articolo 5). Lo script toglie già l'email. Fai lo stesso con numeri di telefono, indirizzi, note interne.
- Il provider del modello riceve i dati. Con un modello online il testo del lead va sui server del fornitore: servono un contratto adeguato e attenzione a dove finiscono i dati. Con Ollama restano sul tuo computer.
- Una decisione automatica non deve restare automatica. Il GDPR (articolo 22) pone limiti alle decisioni basate solo su un trattamento automatizzato che toccano in modo significativo le persone. Un punteggio che decide chi viene richiamato è un uso da tenere sotto controllo: per questo il ramo caldo si ferma per una persona.
- Le email commerciali hanno regole proprie. In Italia, per l'invio di comunicazioni promozionali via email la regola generale è il consenso preventivo. Rispondere a chi ti ha chiesto informazioni è diverso dal fare pubblicità a freddo: se hai dubbi, chiedi a chi si occupa di privacy in azienda.
Per approfondire: il testo del Regolamento UE 2016/679 e la guida del Garante per la protezione dei dati personali.
Come migliorarlo#
- Arricchire il lead. Aggiungi al valutatore
SerperDevTooleScrapeWebsiteTool(capitolo 12) per leggere il sito dell'azienda prima di dare il punteggio. Limitati alle informazioni sull'azienda, non sulla persona. - Un guardrail sulla bozza. Una funzione che rifiuta le email oltre le 120 parole o con cifre in euro, visto che i prezzi non devono essere inventati (capitolo 13).
- Modelli diversi per compiti diversi. Il punteggio è un lavoro breve e ripetitivo: un modello economico o locale può bastare. Per le email che leggeranno i clienti puoi usare un modello più capace, passando un
llmdiverso a ciascun agente. - Dal CSV al CRM, dopo l'approvazione. Il CRM è il programma in cui l'azienda tiene i contatti. Con un server MCP (capitolo 12) puoi salvare la bozza approvata come attività nel CRM invece che in un file. L'invio resta un gesto di una persona.
- Tarare le soglie. Tieni un registro dei punteggi e di chi è diventato cliente davvero. Dopo qualche mese sposta le soglie 70 e 40 sulla base dei dati, non dell'intuito.
Il punteggio è un'opinione del modello, non una misura. Lo stesso lead può prendere 72 una volta e 65 la volta dopo, e finire su un ramo diverso. Leggi le motivazioni, soprattutto per i lead vicini alle soglie.
Mai inviare in automatico. Una email sbagliata a un cliente vero (un prezzo inventato, un nome storpiato, un tono fuori luogo) costa più del tempo risparmiato. Anche quando il flow funziona bene, tieni l'approvazione umana prima dell'invio.
Occhio ai pregiudizi. Un modello può premiare o penalizzare un lead per dettagli irrilevanti, come il settore o il modo di scrivere. I segnali nella valutazione servono proprio a scoprirlo.
Prova tu: sposta le soglie e aggiungi un lead
Aggiungi al CSV un sesto lead inventato: un responsabile IT di un'azienda di 200 dipendenti che scrive «Stiamo confrontando tre fornitori, decidiamo entro due settimane». Poi abbassa la soglia del ramo caldo da 70 a 60. Rilancia: quanti lead caldi ottieni ora, e quante volte ti viene chiesto di approvare?
Soluzione. Il nuovo lead ha segnali forti (urgenza, confronto in corso, azienda grande) e dovrebbe prendere un punteggio alto, quindi finire nel ramo caldo. Abbassando la soglia a 60 può diventare caldo anche un lead come Sara Galli, se il modello le ha dato un punteggio tra 60 e 69. Ogni lead caldo in più è una chiamata in più all'autore e una richiesta di approvazione in più per te. Le soglie sono un compromesso tra non perdere clienti e non sprecare tempo e token: per questo stanno nel codice, in un punto solo, dove puoi cambiarle.
- Qualificare i lead è 7° nella classifica: pochi repository pubblici, ma molto presente nelle storie dei clienti di CrewAI e nel repository ufficiale di esempi.
- Dentro un flow, un passo con un solo agente si fa con
agente.kickoff(..., response_format=...), senza crew. - Il router decide con soglie scritte nel codice; i rami che non servono non chiamano il modello.
human_input=Trueferma il task finché non approvi la bozza nel terminale. Lo script non invia email.- I lead sono dati personali: manda al modello solo il necessario e tieni una persona nel processo.
Nel prossimo esempio lavoriamo con tabelle più grandi: l'analisi di dati.
Corso indipendente, non affiliato a CrewAI Inc. Contenuti verificati su CrewAI 1.15.21 il 15 settembre 2026.